fix(cli): skip NEXT_PUBLIC_KANDEV_API_PORT in production single-port mode (#872)

This commit is contained in:
Carlos Florêncio
2026-05-11 20:32:51 +01:00
committed by GitHub
parent b0e27c3750
commit 71ee104178
2 changed files with 73 additions and 3 deletions
+61
View File
@@ -0,0 +1,61 @@
import { afterEach, describe, expect, it } from "vitest";
import { buildWebEnv, type PortConfig } from "./shared";
const ports: PortConfig = {
backendPort: 38429,
webPort: 37429,
agentctlPort: 36429,
backendUrl: "http://localhost:38429",
};
describe("buildWebEnv", () => {
const originalApiPort = process.env.NEXT_PUBLIC_KANDEV_API_PORT;
afterEach(() => {
if (originalApiPort === undefined) {
delete process.env.NEXT_PUBLIC_KANDEV_API_PORT;
} else {
process.env.NEXT_PUBLIC_KANDEV_API_PORT = originalApiPort;
}
});
it("sets NEXT_PUBLIC_KANDEV_API_PORT in dev mode so the browser knows the backend port", () => {
const env = buildWebEnv({ ports });
expect(env.NEXT_PUBLIC_KANDEV_API_PORT).toBe("38429");
expect(env.NODE_ENV).not.toBe("production");
});
it("does not set NEXT_PUBLIC_KANDEV_API_PORT in production single-port mode", () => {
// The Go backend reverse-proxies Next.js, so the client must use same-origin.
// A non-empty value would cause the client to build cross-origin URLs like
// `https://host:38429/...` that aren't reachable behind a reverse proxy.
const env = buildWebEnv({ ports, production: true });
expect(env.NEXT_PUBLIC_KANDEV_API_PORT).toBeUndefined();
expect(env.NODE_ENV).toBe("production");
});
it("strips a host-level NEXT_PUBLIC_KANDEV_API_PORT in production", () => {
// If an operator has the var in their shell/Docker env, the process.env
// spread would otherwise leak it into the Next.js process and reintroduce
// the cross-origin URL problem this fix addresses.
process.env.NEXT_PUBLIC_KANDEV_API_PORT = "99999";
const env = buildWebEnv({ ports, production: true });
expect(env.NEXT_PUBLIC_KANDEV_API_PORT).toBeUndefined();
});
it("always sets KANDEV_API_BASE_URL for SSR fetches", () => {
expect(buildWebEnv({ ports }).KANDEV_API_BASE_URL).toBe("http://localhost:38429");
expect(buildWebEnv({ ports, production: true }).KANDEV_API_BASE_URL).toBe(
"http://localhost:38429",
);
});
it("enables debug flag when requested", () => {
expect(buildWebEnv({ ports, debug: true }).NEXT_PUBLIC_KANDEV_DEBUG).toBe("true");
expect(buildWebEnv({ ports }).NEXT_PUBLIC_KANDEV_DEBUG).toBeUndefined();
});
});
+12 -3
View File
@@ -85,9 +85,6 @@ export function buildWebEnv(options: WebEnvOptions): NodeJS.ProcessEnv {
...process.env,
// Server-side: full localhost URL for SSR fetches (Next.js SSR → Go backend)
KANDEV_API_BASE_URL: ports.backendUrl,
// Client-side: port injection for dev mode (browser on web port, API on backend port).
// In production, the backend reverse-proxies Next.js so the client uses same-origin.
NEXT_PUBLIC_KANDEV_API_PORT: String(ports.backendPort),
PORT: String(ports.webPort),
// Ensure Next.js standalone server binds to 127.0.0.1 so localhost health checks work.
// Without this, HOSTNAME from the host environment can cause binding issues.
@@ -96,6 +93,18 @@ export function buildWebEnv(options: WebEnvOptions): NodeJS.ProcessEnv {
if (production) {
(env as Record<string, string>).NODE_ENV = "production";
// Explicitly unset so a host-level NEXT_PUBLIC_KANDEV_API_PORT (from a .env
// file, Docker env, or CI variable) cannot leak through the process.env
// spread above and reintroduce the cross-origin URL problem.
delete env.NEXT_PUBLIC_KANDEV_API_PORT;
} else {
// Dev mode only: browser hits the web port directly, so the client needs to
// know the backend port for API calls. In production the Go backend
// reverse-proxies Next.js on a single port, so the client uses same-origin
// and this var must NOT be set — otherwise the client builds cross-origin
// URLs like `https://host:38429/...` that aren't reachable behind a
// reverse proxy / ingress / Cloudflare tunnel.
env.NEXT_PUBLIC_KANDEV_API_PORT = String(ports.backendPort);
}
if (debug) {