Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions src/utils/url-util.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* @license
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 1p-ONLY-IMPORTS: import {afterEach, beforeEach, describe, expect, it}

import {URLUtil} from './url-util';

function fakeLocation(href: string) {
const url = new URL(href);
return {
href: url.href,
origin: url.origin,
pathname: url.pathname,
host: url.host,
};
}

describe('URLUtil', () => {
afterEach(() => {
delete (window as any)['runtimeConfig'];
});

describe('unprefixed deployment (plain `adk web`)', () => {
const location = fakeLocation('http://localhost:8080/dev-ui/session/abc');

it('getBaseUrlWithoutPath returns the server root + dev-ui', () => {
expect(URLUtil.getBaseUrlWithoutPath(location))
.toBe('http://localhost:8080/dev-ui/');
});

it('getApiServerBaseUrl falls back to same-origin (relative) when unset',
() => {
expect(URLUtil.getApiServerBaseUrl(location)).toBe('');
});

it('getWSServerUrl falls back to the current host when unset', () => {
expect(URLUtil.getWSServerUrl(location)).toBe('localhost:8080');
});
});

describe('deployed behind a path-prefixing reverse proxy', () => {
const location = fakeLocation(
'https://wap.example.com/agents/my-agent/dev-ui/session/abc');

it('getBaseUrlWithoutPath preserves the proxy prefix', () => {
expect(URLUtil.getBaseUrlWithoutPath(location))
.toBe('https://wap.example.com/agents/my-agent/dev-ui/');
});

it('getApiServerBaseUrl defaults to same-origin + the proxy prefix ' +
'when runtime-config.json was not customized for this deployment',
() => {
expect(URLUtil.getApiServerBaseUrl(location))
.toBe('https://wap.example.com/agents/my-agent');
});

it('getWSServerUrl strips the scheme off the inferred prefix', () => {
expect(URLUtil.getWSServerUrl(location))
.toBe('wap.example.com/agents/my-agent');
});

it('an explicit runtimeConfig.backendUrl still takes precedence', () => {
(window as any)['runtimeConfig'] = {
backendUrl: 'https://api.example.com',
};
expect(URLUtil.getApiServerBaseUrl(location))
.toBe('https://api.example.com');
});
});
});
50 changes: 41 additions & 9 deletions src/utils/url-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,34 +17,66 @@

import {env} from '../env/env';

/** The subset of `Location` these helpers depend on, for testability. */
type LocationLike = Pick<Location, 'href'|'origin'|'pathname'|'host'>;

export class URLUtil {
/**
* Get the path this app is mounted under, e.g. "/" for a plain `adk web`
* deployment or "/agents/my-agent/" when served behind a path-prefixing
* reverse proxy that forwards ".../agents/my-agent/dev-ui" through
* unmodified. Derived from the current location rather than assumed,
* since the app has no other way to learn a proxy prefix stripped
* upstream of it.
*/
private static getAppRootPath(location: LocationLike): string {
const path = location.pathname;
const devUiIndex = path.indexOf('/dev-ui');
if (devUiIndex < 0) {
return '/';
}
return path.slice(0, devUiIndex) + '/';
}

/**
* Get the base URL without any path
* @returns {string} Base URL (protocol + hostname + port)
*/
static getBaseUrlWithoutPath(): string {
static getBaseUrlWithoutPath(location: LocationLike = window.location):
string {
// Use the URL constructor for robust URL parsing
const currentUrl = window.location.href;
const urlObject = new URL(currentUrl);
const urlObject = new URL(location.href);

// Construct base URL using origin property
// Origin includes protocol, hostname, and port
return urlObject.origin + '/dev-ui/';
return urlObject.origin + URLUtil.getAppRootPath(location) + 'dev-ui/';
}

/**
* Get the base URL without any path
* @returns {string} Base URL (protocol + hostname + port)
*/
static getApiServerBaseUrl(): string {
return (window as any)['runtimeConfig']?.backendUrl || '';
static getApiServerBaseUrl(location: LocationLike = window.location):
string {
const configured = (window as any)['runtimeConfig']?.backendUrl;
if (configured) {
return configured;
}
// No explicit backendUrl configured (runtime-config.json wasn't
// customized for this deployment). Fall back to same-origin, but
// preserve any prefix this app is itself mounted under so API calls
// still reach a reverse proxy that forwards that prefix through
// unmodified — rather than always assuming the API lives at the
// server root.
const root = URLUtil.getAppRootPath(location);
return root === '/' ? '' : location.origin + root.slice(0, -1);
}

static getWSServerUrl(): string {
let url = URLUtil.getApiServerBaseUrl();
static getWSServerUrl(location: LocationLike = window.location): string {
let url = URLUtil.getApiServerBaseUrl(location);
// For adk web, when the api server is not set, use the current host
if (!url || url == '') {
return window.location.host;
return location.host;
}

// For local development, api server address is passed in runtime_config
Expand Down