* fix(frontend): unify gateway-config localhost fallback for prod (#2705) `getGatewayConfig()` only fell back to localhost defaults when `NODE_ENV === "development"`, while `next.config.js` always falls back to `127.0.0.1:8001`. Running `make start` (which sets NODE_ENV=production via `next start`) without `DEER_FLOW_INTERNAL_GATEWAY_BASE_URL` / `DEER_FLOW_TRUSTED_ORIGINS` therefore caused zod to throw inside SSR layouts and surfaced as a 500. Drop the NODE_ENV gating and use localhost defaults everywhere — the "force explicit config in prod" intent should be enforced by deployment templates (docker-compose already sets both vars), not by request-time crashes. Document the two vars in both .env.example files and add unit coverage for the dev/prod env-unset paths. * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Update internalGatewayUrl in gateway config tests --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -48,3 +48,14 @@ INFOQUEST_API_KEY=your-infoquest-api-key
|
|||||||
|
|
||||||
# Set to "false" to disable Swagger UI, ReDoc, and OpenAPI schema in production
|
# Set to "false" to disable Swagger UI, ReDoc, and OpenAPI schema in production
|
||||||
# GATEWAY_ENABLE_DOCS=false
|
# GATEWAY_ENABLE_DOCS=false
|
||||||
|
|
||||||
|
# ── Frontend SSR → Gateway wiring ─────────────────────────────────────────────
|
||||||
|
# The Next.js server uses these to reach the Gateway during SSR (auth checks,
|
||||||
|
# /api/* rewrites). They default to localhost values that match `make dev` and
|
||||||
|
# `make start`, so most local users do not need to set them.
|
||||||
|
#
|
||||||
|
# Override only when the Gateway is not on localhost:8001 (e.g. when the
|
||||||
|
# frontend and gateway run on different hosts, in containers with a service
|
||||||
|
# alias, or behind a different port). docker-compose already sets these.
|
||||||
|
# DEER_FLOW_INTERNAL_GATEWAY_BASE_URL=http://localhost:8001
|
||||||
|
# DEER_FLOW_TRUSTED_ORIGINS=http://localhost:3000,http://localhost:2026
|
||||||
|
|||||||
@@ -14,3 +14,8 @@
|
|||||||
# Only set these if you need to connect to backend services directly
|
# Only set these if you need to connect to backend services directly
|
||||||
# NEXT_PUBLIC_BACKEND_BASE_URL="http://localhost:8001"
|
# NEXT_PUBLIC_BACKEND_BASE_URL="http://localhost:8001"
|
||||||
# NEXT_PUBLIC_LANGGRAPH_BASE_URL="http://localhost:2024"
|
# NEXT_PUBLIC_LANGGRAPH_BASE_URL="http://localhost:2024"
|
||||||
|
|
||||||
|
# Server-only Gateway wiring used by SSR (auth checks, /api/* rewrites).
|
||||||
|
# Defaults to localhost — only override for non-local deployments.
|
||||||
|
# DEER_FLOW_INTERNAL_GATEWAY_BASE_URL="http://localhost:8001"
|
||||||
|
# DEER_FLOW_TRUSTED_ORIGINS="http://localhost:3000,http://localhost:2026"
|
||||||
|
|||||||
@@ -12,12 +12,11 @@ let _cached: GatewayConfig | null = null;
|
|||||||
export function getGatewayConfig(): GatewayConfig {
|
export function getGatewayConfig(): GatewayConfig {
|
||||||
if (_cached) return _cached;
|
if (_cached) return _cached;
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV === "development";
|
|
||||||
|
|
||||||
const rawUrl = process.env.DEER_FLOW_INTERNAL_GATEWAY_BASE_URL?.trim();
|
const rawUrl = process.env.DEER_FLOW_INTERNAL_GATEWAY_BASE_URL?.trim();
|
||||||
const internalGatewayUrl =
|
const internalGatewayUrl =
|
||||||
rawUrl?.replace(/\/+$/, "") ??
|
rawUrl && rawUrl.length > 0
|
||||||
(isDev ? "http://localhost:8001" : undefined);
|
? rawUrl.replace(/\/+$/, "")
|
||||||
|
: "http://127.0.0.1:8001";
|
||||||
|
|
||||||
const rawOrigins = process.env.DEER_FLOW_TRUSTED_ORIGINS?.trim();
|
const rawOrigins = process.env.DEER_FLOW_TRUSTED_ORIGINS?.trim();
|
||||||
const trustedOrigins = rawOrigins
|
const trustedOrigins = rawOrigins
|
||||||
@@ -25,9 +24,7 @@ export function getGatewayConfig(): GatewayConfig {
|
|||||||
.split(",")
|
.split(",")
|
||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
: isDev
|
: ["http://localhost:3000"];
|
||||||
? ["http://localhost:3000"]
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
_cached = gatewayConfigSchema.parse({ internalGatewayUrl, trustedOrigins });
|
_cached = gatewayConfigSchema.parse({ internalGatewayUrl, trustedOrigins });
|
||||||
return _cached;
|
return _cached;
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
|
const ENV_KEYS = [
|
||||||
|
"NODE_ENV",
|
||||||
|
"DEER_FLOW_INTERNAL_GATEWAY_BASE_URL",
|
||||||
|
"DEER_FLOW_TRUSTED_ORIGINS",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
type EnvSnapshot = Partial<
|
||||||
|
Record<(typeof ENV_KEYS)[number], string | undefined>
|
||||||
|
>;
|
||||||
|
|
||||||
|
function snapshotEnv(): EnvSnapshot {
|
||||||
|
const snapshot: EnvSnapshot = {};
|
||||||
|
for (const key of ENV_KEYS) {
|
||||||
|
snapshot[key] = process.env[key];
|
||||||
|
}
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setEnv(key: (typeof ENV_KEYS)[number], value: string | undefined) {
|
||||||
|
// NODE_ENV is typed as a readonly literal union, so we go through the
|
||||||
|
// index signature to keep the test compiler-friendly across cases.
|
||||||
|
const env = process.env as Record<string, string | undefined>;
|
||||||
|
if (value === undefined) {
|
||||||
|
delete env[key];
|
||||||
|
} else {
|
||||||
|
env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreEnv(snapshot: EnvSnapshot) {
|
||||||
|
for (const key of ENV_KEYS) {
|
||||||
|
setEnv(key, snapshot[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadFreshConfig() {
|
||||||
|
vi.resetModules();
|
||||||
|
return await import("@/core/auth/gateway-config");
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("getGatewayConfig", () => {
|
||||||
|
let saved: EnvSnapshot;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
saved = snapshotEnv();
|
||||||
|
setEnv("DEER_FLOW_INTERNAL_GATEWAY_BASE_URL", undefined);
|
||||||
|
setEnv("DEER_FLOW_TRUSTED_ORIGINS", undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
restoreEnv(saved);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns localhost defaults when env is unset in development", async () => {
|
||||||
|
setEnv("NODE_ENV", "development");
|
||||||
|
|
||||||
|
const { getGatewayConfig } = await loadFreshConfig();
|
||||||
|
const cfg = getGatewayConfig();
|
||||||
|
|
||||||
|
expect(cfg.internalGatewayUrl).toBe("http://127.0.0.1:8001");
|
||||||
|
expect(cfg.trustedOrigins).toEqual(["http://localhost:3000"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns localhost defaults when env is unset in production (regression: issue #2705)", async () => {
|
||||||
|
setEnv("NODE_ENV", "production");
|
||||||
|
|
||||||
|
const { getGatewayConfig } = await loadFreshConfig();
|
||||||
|
|
||||||
|
expect(() => getGatewayConfig()).not.toThrow();
|
||||||
|
const cfg = getGatewayConfig();
|
||||||
|
expect(cfg.internalGatewayUrl).toBe("http://127.0.0.1:8001");
|
||||||
|
expect(cfg.trustedOrigins).toEqual(["http://localhost:3000"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses env values verbatim when set, regardless of NODE_ENV", async () => {
|
||||||
|
setEnv("NODE_ENV", "production");
|
||||||
|
setEnv("DEER_FLOW_INTERNAL_GATEWAY_BASE_URL", "https://gw.example.com/");
|
||||||
|
setEnv(
|
||||||
|
"DEER_FLOW_TRUSTED_ORIGINS",
|
||||||
|
"https://app.example.com, https://admin.example.com",
|
||||||
|
);
|
||||||
|
|
||||||
|
const { getGatewayConfig } = await loadFreshConfig();
|
||||||
|
const cfg = getGatewayConfig();
|
||||||
|
|
||||||
|
expect(cfg.internalGatewayUrl).toBe("https://gw.example.com");
|
||||||
|
expect(cfg.trustedOrigins).toEqual([
|
||||||
|
"https://app.example.com",
|
||||||
|
"https://admin.example.com",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("trims and filters empty entries in trustedOrigins", async () => {
|
||||||
|
setEnv("NODE_ENV", "production");
|
||||||
|
setEnv("DEER_FLOW_INTERNAL_GATEWAY_BASE_URL", "https://gw.example.com");
|
||||||
|
setEnv(
|
||||||
|
"DEER_FLOW_TRUSTED_ORIGINS",
|
||||||
|
" https://a.example , ,https://b.example ",
|
||||||
|
);
|
||||||
|
|
||||||
|
const { getGatewayConfig } = await loadFreshConfig();
|
||||||
|
const cfg = getGatewayConfig();
|
||||||
|
|
||||||
|
expect(cfg.trustedOrigins).toEqual([
|
||||||
|
"https://a.example",
|
||||||
|
"https://b.example",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user