mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-28 17:06:05 +00:00
fix(gateway): expose the run metadata header to split-origin clients (#4535)
A browser client served from a different origin than the Gateway never learns the id of the run it just created, so a brand-new thread keeps its placeholder route for the whole session and every action gated on an established thread — edit and rerun, regenerate, branch — stays hidden until the page is reloaded. Run-creating routes return the run's id in `Content-Location`, and the LangGraph SDK resolves run metadata from that header alone. It is not CORS-safelisted, so a cross-origin response hides it from JS unless the server lists it in `Access-Control-Expose-Headers`. `useStream`'s `onCreated` therefore never fires and the app cannot rewrite its route. Expose it. `GATEWAY_CORS_ORIGINS` is a supported deployment mode, so the CORS middleware has to carry everything that mode needs to read. Same-origin nginx deployments are unaffected because CORS never applies to them.
This commit is contained in:
parent
9c7cd4cad3
commit
c24bf383e5
@ -422,7 +422,7 @@ Extensions are optional only in the fallback *search* mode (priority 3-4 above):
|
||||
|
||||
FastAPI application on port 8001 with health check at `GET /health`. Set `GATEWAY_ENABLE_DOCS=false` to disable `/docs`, `/redoc`, and `/openapi.json` in production (default: enabled).
|
||||
|
||||
CORS is same-origin by default when requests enter through nginx on port 2026. Split-origin or port-forwarded browser clients must opt in with `GATEWAY_CORS_ORIGINS` (comma-separated exact origins); Gateway `CORSMiddleware` and `CSRFMiddleware` both read that variable so browser CORS and auth-origin checks stay aligned.
|
||||
CORS is same-origin by default when requests enter through nginx on port 2026. Split-origin or port-forwarded browser clients must opt in with `GATEWAY_CORS_ORIGINS` (comma-separated exact origins); Gateway `CORSMiddleware` and `CSRFMiddleware` both read that variable so browser CORS and auth-origin checks stay aligned. Those clients also need `CORS_EXPOSED_HEADERS` (`csrf_middleware.py`): run-creating routes return the run's id in `Content-Location`, which is not CORS-safelisted, so JS cannot read it unless it is exposed. The LangGraph SDK resolves run metadata from that header alone — withhold it and `useStream`'s `onCreated` never fires, a new thread keeps its placeholder route, and every action gated on an established thread (edit, regenerate, branch) stays hidden until the page is reloaded. Same-origin nginx deployments never hit this because CORS does not apply.
|
||||
|
||||
Browser auth sessions are owned by `app.gateway.auth.session_cookie`. Login accepts a `remember_me` form flag, but the Gateway never stores passwords. `SessionCookiePolicy` persists the `HttpOnly access_token` cookie only for HTTPS/trusted-forwarded HTTPS, direct-host localhost HTTP, or explicit operator opt-in for insecure persistence; public HTTP sandbox URLs degrade to session cookies. Session-creating handlers stamp the final `max_age` on `request.state`, and CSRF cookie creation mirrors that value so the double-submit cookie pair expires together, including explicit re-issue after password changes and OIDC callbacks. A small `HttpOnly` preference cookie preserves the user's remember choice across token re-issue paths. Logout clears all auth cookies and suppresses CSRF re-issue on the logout response.
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ from app.gateway.auth_disabled import warn_if_auth_disabled_enabled
|
||||
from app.gateway.auth_middleware import AuthMiddleware
|
||||
from app.gateway.browser_capability import ensure_browser_runtime_available
|
||||
from app.gateway.config import get_gateway_config
|
||||
from app.gateway.csrf_middleware import CSRFMiddleware, get_configured_cors_origins
|
||||
from app.gateway.csrf_middleware import CORS_EXPOSED_HEADERS, CSRFMiddleware, get_configured_cors_origins
|
||||
from app.gateway.deps import langgraph_runtime
|
||||
from app.gateway.routers import (
|
||||
agents,
|
||||
@ -539,7 +539,11 @@ This gateway provides runtime endpoints for agent runs plus custom endpoints for
|
||||
|
||||
# CORS: the unified nginx endpoint is same-origin by default. Split-origin
|
||||
# browser clients must opt in with this explicit Gateway allowlist so CORS
|
||||
# and CSRF origin checks share the same source of truth.
|
||||
# and CSRF origin checks share the same source of truth. They also need the
|
||||
# run id the Gateway returns in a non-safelisted response header; without
|
||||
# exposing it the SDK never reports a created run, so a new thread keeps its
|
||||
# placeholder route and every action gated on an established thread stays
|
||||
# hidden until the page is reloaded.
|
||||
cors_origins = sorted(get_configured_cors_origins())
|
||||
if cors_origins:
|
||||
app.add_middleware(
|
||||
@ -548,6 +552,7 @@ This gateway provides runtime endpoints for agent runs plus custom endpoints for
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
expose_headers=list(CORS_EXPOSED_HEADERS),
|
||||
)
|
||||
|
||||
# Request trace correlation: when logging.enhance.enabled=true, bind one
|
||||
|
||||
@ -122,6 +122,13 @@ def get_configured_cors_origins() -> set[str]:
|
||||
return _configured_cors_origins()
|
||||
|
||||
|
||||
# Response headers a split-origin browser client must be able to read. Only the
|
||||
# CORS-safelisted set is visible to JS by default, and the created run's id
|
||||
# travels in `Content-Location` — the LangGraph SDK resolves run metadata from
|
||||
# it, so withholding it leaves such a client unable to learn its own run id.
|
||||
CORS_EXPOSED_HEADERS: tuple[str, ...] = ("Content-Location",)
|
||||
|
||||
|
||||
def _first_header_value(value: str | None) -> str | None:
|
||||
"""Return the first value from a comma-separated proxy header."""
|
||||
if not value:
|
||||
|
||||
@ -148,6 +148,21 @@ def test_gateway_cors_allows_configured_origin():
|
||||
assert response.headers["access-control-allow-credentials"] == "true"
|
||||
|
||||
|
||||
def test_gateway_cors_exposes_the_run_metadata_header():
|
||||
"""`Content-Location` carries the created run id and is not CORS-safelisted.
|
||||
|
||||
A split-origin browser client that cannot read it never learns its own run
|
||||
id, so the SDK reports no created run and the thread keeps its placeholder
|
||||
route for the whole session.
|
||||
"""
|
||||
client = _make_gateway_client("https://app.example")
|
||||
|
||||
response = client.get("/health", headers={"Origin": "https://app.example"})
|
||||
|
||||
exposed = {value.strip().lower() for value in response.headers.get("access-control-expose-headers", "").split(",")}
|
||||
assert "content-location" in exposed
|
||||
|
||||
|
||||
def test_gateway_cors_rejects_unconfigured_origin():
|
||||
client = _make_gateway_client("https://app.example")
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user