diff --git a/apps/sim/lib/core/utils/urls.test.ts b/apps/sim/lib/core/utils/urls.test.ts index b476f76d073..bcbe797df6e 100644 --- a/apps/sim/lib/core/utils/urls.test.ts +++ b/apps/sim/lib/core/utils/urls.test.ts @@ -56,15 +56,20 @@ describe('getBaseUrl', () => { expect(getBaseUrl()).toBe('https://app.example.com') }) - it('falls back to the page origin instead of throwing when the injected env is missing', () => { + /** + * Never guesses from `window.location.origin`: an opaque origin (a sandboxed + * iframe) serializes to the truthy string `'null'`, which would silently + * produce `null/api/...` rather than surfacing the misconfiguration. + */ + it('throws in the browser rather than guessing from the page origin', () => { setLocation('https://www.sim.ai/workspace/ws-1/w/wf-1') - expect(getBaseUrl()).toBe('https://www.sim.ai') + expect(() => getBaseUrl()).toThrow('NEXT_PUBLIC_APP_URL must be configured') }) it('treats a whitespace-only NEXT_PUBLIC_APP_URL as unset', () => { mockGetEnv.mockImplementation((key) => (key === 'NEXT_PUBLIC_APP_URL' ? ' ' : undefined)) setLocation('https://www.sim.ai/') - expect(getBaseUrl()).toBe('https://www.sim.ai') + expect(() => getBaseUrl()).toThrow('NEXT_PUBLIC_APP_URL must be configured') }) }) diff --git a/apps/sim/lib/core/utils/urls.ts b/apps/sim/lib/core/utils/urls.ts index 59abca57512..5f13016e80b 100644 --- a/apps/sim/lib/core/utils/urls.ts +++ b/apps/sim/lib/core/utils/urls.ts @@ -25,31 +25,26 @@ function normalizeBaseUrl(url: string): string { * Returns the base URL of the application from NEXT_PUBLIC_APP_URL * This ensures webhooks, callbacks, and other integrations always use the correct public URL * - * In the browser, falls back to the page's own origin when the injected env is - * unavailable. Client-side callers only ever want a URL back to the app they are - * already served from, so the origin is a correct answer — and a throw here - * during render tears down the whole page through the error boundary. Server-side - * callers (webhooks, callbacks, emails) have no origin to fall back to and must - * still fail loudly on a misconfigured deployment. + * Deliberately has no browser fallback to `window.location.origin`. The value is + * injected before hydration by ``, so an empty read means the + * deployment is misconfigured — and a same-origin guess would hide that. It also + * would not be safe to guess: an opaque origin (a sandboxed iframe, and `/chat/*` + * is embeddable) serializes to the string `'null'`, which is truthy and would + * silently produce `null/api/...` at every call site. * * @returns The base URL string (e.g., 'http://localhost:3000' or 'https://example.com') - * @throws Error if NEXT_PUBLIC_APP_URL is not configured and no browser origin exists + * @throws Error if NEXT_PUBLIC_APP_URL is not configured */ export function getBaseUrl(): string { const baseUrl = getEnv('NEXT_PUBLIC_APP_URL')?.trim() - if (baseUrl) { - return normalizeBaseUrl(baseUrl) - } - - const browserOrigin = getBrowserOrigin() - if (browserOrigin) { - return browserOrigin + if (!baseUrl) { + throw new Error( + 'NEXT_PUBLIC_APP_URL must be configured for webhooks and callbacks to work correctly' + ) } - throw new Error( - 'NEXT_PUBLIC_APP_URL must be configured for webhooks and callbacks to work correctly' - ) + return normalizeBaseUrl(baseUrl) } /** diff --git a/packages/testing/src/mocks/urls.mock.ts b/packages/testing/src/mocks/urls.mock.ts index ab013442723..2dab1b1560e 100644 --- a/packages/testing/src/mocks/urls.mock.ts +++ b/packages/testing/src/mocks/urls.mock.ts @@ -26,18 +26,14 @@ function hasHttpProtocol(url: string): boolean { function getBaseUrlImpl(): string { const baseUrl = readEnv('NEXT_PUBLIC_APP_URL')?.trim() - if (baseUrl) { - // Mirrors the real module: protocol-less values get https:// under isProd. - const protocol = envFlagsMock.isProd ? 'https://' : 'http://' - return hasHttpProtocol(baseUrl) ? baseUrl : `${protocol}${baseUrl}` + if (!baseUrl) { + throw new Error( + 'NEXT_PUBLIC_APP_URL must be configured for webhooks and callbacks to work correctly' + ) } - // Mirrors the real module: the browser falls back to its own origin, only - // server-side (no `window`) callers throw. - const browserOrigin = getBrowserOriginImpl() - if (browserOrigin) return browserOrigin - throw new Error( - 'NEXT_PUBLIC_APP_URL must be configured for webhooks and callbacks to work correctly' - ) + // Mirrors the real module: protocol-less values get https:// under isProd. + const protocol = envFlagsMock.isProd ? 'https://' : 'http://' + return hasHttpProtocol(baseUrl) ? baseUrl : `${protocol}${baseUrl}` } function getInternalApiBaseUrlImpl(): string {