mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
fix(helm): forward Live Browser WebSocket upgrades (#7148)
* fix(helm): proxy Live Browser WebSocket upgrades * fix(helm): use default browser connect and send timeouts
This commit is contained in:
parent
53f09dc152
commit
2c880a3133
@ -31,6 +31,11 @@ Nginx is the single public entry: it proxies `/api/*` to the Gateway, rewriting
|
||||
HTML and configured textual assets, deliberately leaving SSE, fonts, images, audio, and
|
||||
video uncompressed at the proxy layer.
|
||||
|
||||
Keep the dedicated `/api/threads/{thread_id}/browser/stream` WebSocket regex before
|
||||
the generic threads regex in Docker, local and Helm nginx configs. Upgrade headers
|
||||
and the 600s read timeout are pinned by `backend/tests/test_nginx_browser_websocket.py`.
|
||||
Helm uses nginx's default connect/send timeouts for the in-cluster Gateway.
|
||||
|
||||
Both Compose files publish nginx at `"${BIND_HOST:-127.0.0.1}:${PORT:-2026}:2026"`
|
||||
(README's **loopback default**); bare `"${PORT}:2026"` binds `0.0.0.0`. Root `PORT`
|
||||
controls Docker ingress only; local Next.js stays on `3000` so `.env` cannot
|
||||
|
||||
@ -587,6 +587,8 @@ The unified nginx endpoint is same-origin by default and does not emit browser C
|
||||
|
||||
When fine-grained authorization is enabled, Live Browser connections require `threads:write` as well as ownership of the thread, even when only viewing frames: the same connection can control the browser. Permission checks run when connecting. Restart Gateway after upgrading to disconnect sessions admitted by older code.
|
||||
|
||||
The bundled Docker, local and Helm nginx configs proxy Live Browser WebSocket upgrades with a 600-second read timeout for quiet streams. Helm uses nginx's default connection and send timeouts for the in-cluster Gateway. Any additional Ingress or reverse proxy must also allow WebSocket upgrades.
|
||||
|
||||
Browser login uses `HttpOnly` session cookies. The login page offers a "keep me signed in" option that extends the browser session when the request is HTTPS (including trusted `X-Forwarded-Proto: https`) or localhost HTTP. The localhost exception uses the direct request `Host` and ignores forwarded host headers. Public HTTP deployments, including many temporary sandbox URLs, fall back to session cookies by default. DeerFlow never stores the password in browser storage; the UI may remember only the email address.
|
||||
|
||||
OIDC/SSO authorization endpoints may include provider query parameters such as `?prompt=login`. DeerFlow preserves those parameters when adding the login request, including for configured `authorization_endpoint` overrides.
|
||||
|
||||
61
backend/tests/test_nginx_browser_websocket.py
Normal file
61
backend/tests/test_nginx_browser_websocket.py
Normal file
@ -0,0 +1,61 @@
|
||||
"""Live Browser upgrades must reach Gateway in every maintained nginx config."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
from support.nginx_conf import NGINX_CONFIGS, extract_location_block, read_config
|
||||
|
||||
CHART = Path(__file__).resolve().parents[2] / "deploy" / "helm" / "deer-flow"
|
||||
BROWSER_PATTERN = r"^/api/threads/[^/]+/browser/stream"
|
||||
|
||||
|
||||
def _assert_browser_websocket_config(config: str, *, helm_defaults: bool = False) -> None:
|
||||
block = extract_location_block(config, f"~ {BROWSER_PATTERN}")
|
||||
|
||||
# nginx selects the first matching regex, so the generic threads location
|
||||
# must not swallow the WebSocket handshake before this location is reached.
|
||||
regex_locations = re.findall(r"^\s*location\s+~\s+([^\{]+)\{", config, re.M)
|
||||
first_match = next((pattern.strip() for pattern in regex_locations if re.match(pattern.strip(), "/api/threads/thread-42/browser/stream")), None)
|
||||
assert first_match == BROWSER_PATTERN
|
||||
|
||||
assert re.search(r"^\s*proxy_pass http://\$?gateway(?:_upstream)?;\s*$", block, re.M)
|
||||
assert re.search(r"^\s*proxy_set_header X-Forwarded-Proto \$(?:forwarded_proto|scheme);\s*$", block, re.M)
|
||||
for directive in (
|
||||
"proxy_http_version 1.1;",
|
||||
"proxy_set_header Host $http_host;",
|
||||
"proxy_set_header X-Real-IP $remote_addr;",
|
||||
"proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;",
|
||||
"proxy_set_header Upgrade $http_upgrade;",
|
||||
"proxy_set_header Connection 'upgrade';",
|
||||
"proxy_cache_bypass $http_upgrade;",
|
||||
"proxy_read_timeout 600s;",
|
||||
):
|
||||
assert re.search(r"^\s*" + re.escape(directive) + r"\s*$", block, re.M), f"Live Browser location is missing {directive}"
|
||||
|
||||
if helm_defaults:
|
||||
# Connecting to Gateway and sending the upgrade request use nginx's 60s
|
||||
# defaults; the read timeout above keeps quiet WebSockets open.
|
||||
for directive in ("proxy_connect_timeout", "proxy_send_timeout"):
|
||||
assert not re.search(r"^\s*" + directive + r"\s+", block, re.M), f"Helm Live Browser location should use the default {directive}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", NGINX_CONFIGS)
|
||||
def test_live_browser_websocket_config(path: str) -> None:
|
||||
_assert_browser_websocket_config(read_config(path), helm_defaults=path.startswith("deploy/helm/"))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("provisioner_enabled", [True, False])
|
||||
def test_rendered_helm_live_browser_websocket_config(provisioner_enabled: bool) -> None:
|
||||
helm = shutil.which("helm")
|
||||
if helm is None:
|
||||
pytest.skip("helm is unavailable")
|
||||
command = [helm, "template", "deer-flow", str(CHART), "--show-only", "templates/configmap-nginx.yaml", "--set", f"provisioner.enabled={str(provisioner_enabled).lower()}"]
|
||||
rendered = subprocess.run(command, check=True, capture_output=True, text=True, encoding="utf-8").stdout
|
||||
config_map = yaml.safe_load(rendered)
|
||||
_assert_browser_websocket_config(config_map["data"]["nginx.conf"], helm_defaults=True)
|
||||
@ -204,6 +204,24 @@ data:
|
||||
proxy_read_timeout 600s;
|
||||
}
|
||||
|
||||
# Live browser stream is a WebSocket upgrade. Match it before the
|
||||
# generic /api/threads regex so the handshake reaches Gateway with
|
||||
# the upgrade headers intact.
|
||||
location ~ ^/api/threads/[^/]+/browser/stream {
|
||||
proxy_pass http://gateway_upstream;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $forwarded_proto;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
|
||||
# Allow quiet streams; connection and send timeouts use nginx defaults.
|
||||
proxy_read_timeout 600s;
|
||||
}
|
||||
|
||||
location ~ ^/api/threads {
|
||||
proxy_pass http://gateway_upstream;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user