diff --git a/web/src/lib/backend-config.test.ts b/web/src/lib/backend-config.test.ts index 4775d3d1..c19e77e9 100644 --- a/web/src/lib/backend-config.test.ts +++ b/web/src/lib/backend-config.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { BackendStorageError, buildTransportConfig, @@ -109,3 +109,41 @@ describe('buildTransportConfig', () => { expect(buildTransportConfig()?.wsURL).toBe('ws://localhost:9880/ws'); }); }); + +describe('VITE_BACKEND_URL build-time fallback', () => { + afterEach(() => { + vi.unstubAllEnvs(); + vi.resetModules(); + localStorage.clear(); + }); + + it('ignores a non-http(s) VITE_BACKEND_URL (ftp://) and returns undefined', async () => { + vi.stubEnv('VITE_BACKEND_URL', 'ftp://api.example.com'); + vi.resetModules(); + const { buildTransportConfig: btc } = await import('./backend-config'); + // No localStorage override, no valid build-time URL → same-origin default. + expect(btc()).toBeUndefined(); + }); + + it('uses a valid https VITE_BACKEND_URL as the fallback', async () => { + vi.stubEnv('VITE_BACKEND_URL', 'https://api.example.com'); + vi.resetModules(); + const { buildTransportConfig: btc } = await import('./backend-config'); + expect(btc()).toEqual({ + baseURL: 'https://api.example.com/api', + adminBaseURL: 'https://api.example.com/api/admin', + wsURL: 'wss://api.example.com/ws', + }); + }); + + it('normalizes VITE_BACKEND_URL by dropping query/hash and trailing slashes', async () => { + vi.stubEnv('VITE_BACKEND_URL', 'https://api.example.com/maxx/?x=1#frag'); + vi.resetModules(); + const { buildTransportConfig: btc } = await import('./backend-config'); + expect(btc()).toEqual({ + baseURL: 'https://api.example.com/maxx/api', + adminBaseURL: 'https://api.example.com/maxx/api/admin', + wsURL: 'wss://api.example.com/maxx/ws', + }); + }); +}); diff --git a/web/src/lib/backend-config.ts b/web/src/lib/backend-config.ts index 36999c99..240d108d 100644 --- a/web/src/lib/backend-config.ts +++ b/web/src/lib/backend-config.ts @@ -34,13 +34,34 @@ 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 the same way setBackendUrl() does: + * validates the protocol is http(s), drops query/hash, strips trailing slashes. + * Returns an empty string for invalid, empty, or non-http(s) inputs so callers + * can treat the result as "no override" (same-origin default). + */ +function normalizeBackendUrl(raw: string): string { + const trimmed = raw.trim(); + if (!trimmed) return ''; + try { + const parsed = new URL(trimmed); + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + return ''; + } + return (parsed.origin + parsed.pathname).replace(/\/+$/, ''); + } catch { + return ''; + } +} + +/** Build-time fallback (empty string when unset or invalid). */ +const BUILD_TIME_BACKEND_URL: string = normalizeBackendUrl( + (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 = '';