From 1b8c079c0d39d2c6d90c8c5fde31b5db18e60c79 Mon Sep 17 00:00:00 2001 From: Script Generator Date: Mon, 6 Jul 2026 18:06:48 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20=E7=94=9F=E4=BA=A7=E7=8E=AF?= =?UTF-8?q?=E5=A2=83=E5=BF=BD=E7=95=A5=E6=9C=AC=E6=9C=BA=20API=20=E5=9C=B0?= =?UTF-8?q?=E5=9D=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/lib/api.test.ts | 13 +++++++++++++ apps/web/lib/api.ts | 15 ++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/apps/web/lib/api.test.ts b/apps/web/lib/api.test.ts index f1e9fcb..603b6d9 100644 --- a/apps/web/lib/api.test.ts +++ b/apps/web/lib/api.test.ts @@ -12,6 +12,19 @@ test('API base defaults to same-origin in production and localhost in developmen ); }); +test('API base ignores local-only absolute URLs in production', () => { + for (const localApiUrl of [ + 'http://localhost:3001/api/v1', + 'http://127.0.0.1:3001/api/v1', + 'http://0.0.0.0:3001/api/v1', + ]) { + assert.equal( + resolveApiBase({ NODE_ENV: 'production', NEXT_PUBLIC_API_URL: localApiUrl }), + '/api/v1', + ); + } +}); + test('API requests use the resolved base path', async () => { const originalFetch = globalThis.fetch; const apiBase = resolveApiBase(); diff --git a/apps/web/lib/api.ts b/apps/web/lib/api.ts index 30a4e0c..dd2ebf0 100644 --- a/apps/web/lib/api.ts +++ b/apps/web/lib/api.ts @@ -5,11 +5,24 @@ type ApiEnv = { export function resolveApiBase(env: ApiEnv = process.env) { const fallback = env.NODE_ENV === 'production' ? '/api/v1' : 'http://localhost:3001/api/v1'; - return (env.NEXT_PUBLIC_API_URL || fallback).replace(/\/$/, ''); + const configured = (env.NEXT_PUBLIC_API_URL || fallback).replace(/\/$/, ''); + if (env.NODE_ENV === 'production' && isLocalOnlyApiUrl(configured)) { + return '/api/v1'; + } + return configured; } const API_BASE = resolveApiBase(); +function isLocalOnlyApiUrl(value: string) { + try { + const url = new URL(value); + return ['localhost', '127.0.0.1', '0.0.0.0'].includes(url.hostname); + } catch { + return false; + } +} + let apiAvailable: boolean | null = null; let probePromise: Promise | null = null;