Skip to content
Closed
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
36 changes: 36 additions & 0 deletions web/src/lib/backend-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,39 @@ describe('buildTransportConfig', () => {
expect(buildTransportConfig()?.wsURL).toBe('ws://localhost:9880/ws');
});
});

describe('VITE_BACKEND_URL build-time fallback normalization', () => {
it('normalizes a VITE_BACKEND_URL that contains query and hash', async () => {
// Re-import the module with a stubbed env so the module-level constant is
// re-evaluated. vi.resetModules() clears the module registry, and
// vi.stubEnv sets import.meta.env before the fresh import runs.
vi.stubEnv('VITE_BACKEND_URL', 'https://api.example.com?x=1#frag');
vi.resetModules();
const mod = await import('./backend-config');

// No localStorage override → falls back to BUILD_TIME_BACKEND_URL.
localStorage.clear();
const backend = mod.getBackendUrl();
expect(backend).toBe('https://api.example.com');

const cfg = mod.buildTransportConfig();
expect(cfg).not.toBeUndefined();
expect(cfg!.baseURL).toBe('https://api.example.com/api');
expect(cfg!.wsURL).toBe('wss://api.example.com/ws');

vi.unstubAllEnvs();
vi.resetModules();
});

it('normalizes a VITE_BACKEND_URL with trailing slash', async () => {
vi.stubEnv('VITE_BACKEND_URL', 'https://api.example.com/');
vi.resetModules();
const mod = await import('./backend-config');

localStorage.clear();
expect(mod.getBackendUrl()).toBe('https://api.example.com');

vi.unstubAllEnvs();
vi.resetModules();
});
});
26 changes: 22 additions & 4 deletions web/src/lib/backend-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,31 @@ export class BackendStorageError extends Error {
}
}

/** Build-time fallback (empty string when unset). */
const BUILD_TIME_BACKEND_URL: string =
(import.meta.env.VITE_BACKEND_URL as string | undefined)?.trim() ?? '';
/**
* Normalizes a raw backend URL string to `origin + pathname` with no trailing
* slash and no query/hash. Returns an empty string for empty/whitespace input
* or unparseable values. This is the same contract as setBackendUrl so that
* VITE_BACKEND_URL and runtime overrides share identical semantics.
*/
function normalizeBackendOrigin(raw: string): string {
const trimmed = raw.trim();
if (!trimmed) return '';
try {
const parsed = new URL(trimmed);
return (parsed.origin + parsed.pathname).replace(/\/+$/, '');
} catch {
return '';
}
}

/** Build-time fallback, fully normalized (empty string when unset or unparseable). */
const BUILD_TIME_BACKEND_URL: string = normalizeBackendOrigin(
(import.meta.env.VITE_BACKEND_URL as string | undefined) ?? '',
);

/**
* Returns the configured backend base origin (no trailing slash), or an empty
* string when the UI should use its own origin (same-origin default).

*/
export function getBackendUrl(): string {
let stored = '';
Expand Down