mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-27 08:28:00 +00:00
fix(frontend): allow dev-server access from non-localhost hosts (#4471)
Opening the dev stack on a LAN address or a proxied hostname serves the SSR HTML but never hydrates: Next.js answers /_next/*, /__nextjs_font/*, and HMR with 403 for any host it was not started on. The page renders, so it looks up — but no client handler is attached, and the login form's onSubmit never fires. It reads as "login is broken" rather than as an asset problem, and the only clue is a warning in the dev-server log. Wire Next's allowedDevOrigins to a new DEER_FLOW_DEV_ALLOWED_ORIGINS env var. Unset by default, so the localhost-only default is unchanged; it is also dev-only, as Next ignores allowedDevOrigins in production builds. Entries are reduced to the bare host that allowedDevOrigins matches against, since an entry pasted from the address bar as "http://192.168.1.10:2026" would otherwise match nothing and leave the operator with the same 403 they were trying to fix. Reported in #54 and #203. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
55c2153080
commit
e17aff57a0
@ -20,3 +20,12 @@
|
||||
# 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"
|
||||
|
||||
# Extra hosts allowed to load Next.js dev-server resources (`/_next/*`, fonts,
|
||||
# HMR). Development only — production builds ignore it.
|
||||
# Needed to open `pnpm dev` / `make docker-start` on anything other than
|
||||
# localhost, such as a LAN address or a proxied hostname: without it Next.js
|
||||
# answers those requests with 403, so the page renders server-side but never
|
||||
# hydrates and no interaction (including the login form) works.
|
||||
# Comma-separated; a full URL is reduced to its host.
|
||||
# DEER_FLOW_DEV_ALLOWED_ORIGINS="192.168.1.10,*.local"
|
||||
|
||||
@ -128,6 +128,8 @@ NEXT_PUBLIC_LANGGRAPH_BASE_URL=http://localhost:8001/api
|
||||
|
||||
Leave these unset for the standard `make dev` / Docker flow, where nginx serves the public `/api/langgraph/*` prefix and rewrites it to Gateway's native `/api/*` routes.
|
||||
|
||||
To reach a dev server on anything other than localhost — a LAN address, or a proxied hostname — list the host in `DEER_FLOW_DEV_ALLOWED_ORIGINS` (comma-separated; a full URL is reduced to its host). It feeds Next's `allowedDevOrigins`, which gates `/_next/*`, fonts, and HMR. Without it those requests get a 403 and the page renders server-side but never hydrates, so nothing on it — including the login form — responds. Development only; production builds ignore it.
|
||||
|
||||
## Resources
|
||||
|
||||
- [LangGraph Documentation](https://langchain-ai.github.io/langgraph/)
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* for Docker builds.
|
||||
*/
|
||||
import "./src/env.js";
|
||||
import { getAllowedDevOrigins } from "./src/dev-origins.js";
|
||||
|
||||
function getInternalServiceURL(envKey, fallbackURL) {
|
||||
const configured = process.env[envKey]?.trim();
|
||||
@ -25,6 +26,7 @@ const config = {
|
||||
defaultLocale: "en",
|
||||
},
|
||||
devIndicators: false,
|
||||
allowedDevOrigins: getAllowedDevOrigins(),
|
||||
async rewrites() {
|
||||
const rewrites = [];
|
||||
const gatewayURL = getInternalServiceURL(
|
||||
|
||||
59
frontend/src/dev-origins.js
Normal file
59
frontend/src/dev-origins.js
Normal file
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Hosts allowed to load Next.js dev-server resources (`/_next/*`,
|
||||
* `/__nextjs_font/*`, HMR) from an origin other than the one `pnpm dev` was
|
||||
* started on.
|
||||
*
|
||||
* Next.js answers those requests with 403 unless the host is listed, so a dev
|
||||
* stack opened on a LAN address or a proxied hostname serves the SSR HTML but
|
||||
* never hydrates: the page renders and nothing on it responds.
|
||||
*
|
||||
* Dev-only — Next ignores `allowedDevOrigins` in production builds.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Reduce one entry to the bare host `allowedDevOrigins` matches against.
|
||||
*
|
||||
* Next matches on host alone, so an entry that still carries a scheme, port, or
|
||||
* path matches nothing and leaves the caller with the same 403 they were trying
|
||||
* to fix. Accept the URL people naturally copy out of the address bar.
|
||||
*
|
||||
* @param {string} value
|
||||
* @returns {string} bare host, or `""` if the entry was empty
|
||||
*/
|
||||
function normalizeHost(value) {
|
||||
let host = value.trim();
|
||||
if (!host) return "";
|
||||
|
||||
host = host.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "");
|
||||
host = host.replace(/[/?#].*$/, "");
|
||||
|
||||
const bracketedIpv6 = /^\[([^\]]+)\](?::\d+)?$/.exec(host);
|
||||
if (bracketedIpv6) return bracketedIpv6[1];
|
||||
|
||||
// A bare IPv6 literal has several colons and no port to strip; only a single
|
||||
// colon can be a `host:port` separator.
|
||||
if ((host.match(/:/g) ?? []).length === 1) {
|
||||
host = host.replace(/:\d+$/, "");
|
||||
}
|
||||
return host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a comma-separated host list into the shape `allowedDevOrigins` expects.
|
||||
*
|
||||
* @param {string | undefined} raw
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function parseAllowedDevOrigins(raw) {
|
||||
return (raw ?? "").split(",").map(normalizeHost).filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the configured hosts from the environment.
|
||||
*
|
||||
* @param {Record<string, string | undefined>} [env]
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function getAllowedDevOrigins(env = process.env) {
|
||||
return parseAllowedDevOrigins(env.DEER_FLOW_DEV_ALLOWED_ORIGINS);
|
||||
}
|
||||
69
frontend/tests/unit/dev-origins.test.ts
Normal file
69
frontend/tests/unit/dev-origins.test.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { describe, expect, test } from "@rstest/core";
|
||||
|
||||
import { getAllowedDevOrigins, parseAllowedDevOrigins } from "@/dev-origins";
|
||||
|
||||
describe("parseAllowedDevOrigins", () => {
|
||||
test("returns an empty list when unset or empty", () => {
|
||||
expect(parseAllowedDevOrigins(undefined)).toEqual([]);
|
||||
expect(parseAllowedDevOrigins("")).toEqual([]);
|
||||
expect(parseAllowedDevOrigins(" ")).toEqual([]);
|
||||
});
|
||||
|
||||
test("splits a comma-separated list and trims each entry", () => {
|
||||
expect(parseAllowedDevOrigins(" 192.168.1.10 , dev.example.com ")).toEqual([
|
||||
"192.168.1.10",
|
||||
"dev.example.com",
|
||||
]);
|
||||
});
|
||||
|
||||
test("drops empty entries from trailing or doubled commas", () => {
|
||||
expect(parseAllowedDevOrigins("a.example,,b.example,")).toEqual([
|
||||
"a.example",
|
||||
"b.example",
|
||||
]);
|
||||
});
|
||||
|
||||
test("reduces a pasted URL to the bare host Next matches on", () => {
|
||||
expect(parseAllowedDevOrigins("http://192.168.1.10:2026")).toEqual([
|
||||
"192.168.1.10",
|
||||
]);
|
||||
expect(parseAllowedDevOrigins("https://dev.example.com/")).toEqual([
|
||||
"dev.example.com",
|
||||
]);
|
||||
expect(parseAllowedDevOrigins("http://dev.example.com/login?x=1")).toEqual([
|
||||
"dev.example.com",
|
||||
]);
|
||||
});
|
||||
|
||||
test("preserves wildcard patterns", () => {
|
||||
expect(parseAllowedDevOrigins("*.local, *.example.com")).toEqual([
|
||||
"*.local",
|
||||
"*.example.com",
|
||||
]);
|
||||
});
|
||||
|
||||
test("strips the port from a bracketed IPv6 host without mangling the address", () => {
|
||||
expect(parseAllowedDevOrigins("[::1]:2026")).toEqual(["::1"]);
|
||||
expect(parseAllowedDevOrigins("http://[fe80::1]:3000")).toEqual([
|
||||
"fe80::1",
|
||||
]);
|
||||
});
|
||||
|
||||
test("leaves a bare IPv6 literal intact", () => {
|
||||
// Several colons and no port to strip — treating the last group as a port
|
||||
// would corrupt the address.
|
||||
expect(parseAllowedDevOrigins("fe80::1")).toEqual(["fe80::1"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAllowedDevOrigins", () => {
|
||||
test("reads DEER_FLOW_DEV_ALLOWED_ORIGINS", () => {
|
||||
expect(
|
||||
getAllowedDevOrigins({ DEER_FLOW_DEV_ALLOWED_ORIGINS: "192.168.1.10" }),
|
||||
).toEqual(["192.168.1.10"]);
|
||||
});
|
||||
|
||||
test("defaults to an empty list, keeping localhost-only the default", () => {
|
||||
expect(getAllowedDevOrigins({})).toEqual([]);
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user