From 4c43219287d629e650bca8a652bd576b0c3b609c Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 11:11:05 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20CodeRabbit=20Chat:=20Implement?= =?UTF-8?q?=20requested=20code=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/lib/backend-config.test.ts | 40 +++++++++++++++++++++++++++++- web/src/lib/backend-config.ts | 29 +++++++++++++++++++--- 2 files changed, 64 insertions(+), 5 deletions(-) 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 = '';