mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
* fix(nginx): canonicalize the loopback origin onto localhost localhost and 127.0.0.1 (or [::1]) are the same loopback listener but separate cookie jars to a browser, so an admin who sets up on one spelling lands on /login when they later open the other, with no hint why (#6156). Cookies cannot bridge the gap: no Domain attribute spans both hosts. All three nginx configs now map the numeric spellings to "localhost", the name every banner and doc already prints, and the main server answers a 301 to the same URL on the canonical host. The published port survives because the map keys on $http_host and reattaches the captured port; Docker's arbitrary host-port mapping is unaffected. Safe methods only: GET/HEAD redirect, while API POSTs pass through untouched, since a redirected POST strands clients that do not re-send bodies. Non-loopback Hosts (LAN names, ingress domains) never match, and the canonical target itself is not a match, so there is no redirect loop. The new suite parses both maps out of each config and evaluates the full method x host matrix against nginx semantics, including the suffix-spoof and redirect-loop cases, and pins the maps identical across the Docker, local-dev, and Helm configs so they cannot drift. Fixes #6156 Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com> * docs: keep the loopback note in the nginx configs The root AGENTS.md paragraph pushed the middlewares and sandbox instruction chains over the 96 KiB hard limit. The nginx configs already carry the full explanation next to the maps, so drop the duplicated paragraph instead of shrinking the budgets. * fix(nginx): let WebSocket upgrades bypass the loopback canonical redirect willem-bd caught this running the stack locally: the canonicalization map keyed on method:host alone, so a GET with an Upgrade header got the same 301 as a plain GET. WebSocket clients do not follow redirects, so every WS handshake against a 127.0.0.1/[::1] spelling died at the redirect. Key the origin map on method:upgrade:host instead. Handshakes carry a non-empty Upgrade value, fall off the GET::/HEAD:: patterns, and proxy through untouched. Plain GET/HEAD behavior is unchanged. --------- Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
419 lines
16 KiB
Plaintext
419 lines
16 KiB
Plaintext
# Global error_log (main context). Without this, nginx opens its compiled-in
|
|
# default (/var/log/nginx/error.log) at startup before reaching the http-block
|
|
# error_log, which fails with permission denied when run as a non-root user.
|
|
error_log logs/nginx-error.log warn;
|
|
events {
|
|
worker_connections 1024;
|
|
}
|
|
pid logs/nginx.pid;
|
|
http {
|
|
# Basic settings
|
|
sendfile on;
|
|
tcp_nopush on;
|
|
tcp_nodelay on;
|
|
keepalive_timeout 65;
|
|
types_hash_max_size 2048;
|
|
|
|
# Compress textual delivery only. SSE and already-compressed media are
|
|
# deliberately absent because buffering/compression hurts their latency.
|
|
gzip on;
|
|
gzip_vary on;
|
|
gzip_proxied any;
|
|
gzip_min_length 1024;
|
|
gzip_comp_level 5;
|
|
gzip_types
|
|
text/css
|
|
text/javascript
|
|
application/javascript
|
|
application/json
|
|
application/xml
|
|
image/svg+xml;
|
|
|
|
# Logging
|
|
access_log logs/nginx-access.log;
|
|
error_log logs/nginx-error.log;
|
|
|
|
# Upstream servers (using 127.0.0.1 for local development)
|
|
upstream gateway {
|
|
server 127.0.0.1:8001;
|
|
}
|
|
|
|
upstream frontend {
|
|
server 127.0.0.1:3000;
|
|
}
|
|
|
|
# Only mark frontend requests as connection upgrades when the browser
|
|
# actually requested one. Next.js dev HMR also uses long-lived HTTP
|
|
# streams, and forcing "Connection: upgrade" on those requests makes
|
|
# nginx treat the upstream response as an invalid upgrade handshake.
|
|
map $http_upgrade $connection_upgrade {
|
|
default upgrade;
|
|
'' '';
|
|
}
|
|
|
|
# Loopback has two spellings and browsers scope cookies per host, so
|
|
# serving both "localhost" and "127.0.0.1" (or "[::1]") splits the login
|
|
# session into separate jars (#6156). Canonicalize on "localhost", the
|
|
# spelling every banner and doc prints, keeping the caller's port.
|
|
map $http_host $loopback_canonical_host {
|
|
default "";
|
|
"127.0.0.1" "localhost";
|
|
"~*^127\.0\.0\.1:(?<loopback_port>\d+)$" "localhost:$loopback_port";
|
|
"[::1]" "localhost";
|
|
"~*^\[::1\]:(?<loopback_port>\d+)$" "localhost:$loopback_port";
|
|
}
|
|
|
|
# Safe methods only: redirecting an API POST would strand clients that
|
|
# do not re-send bodies across the redirect. A GET carrying an Upgrade
|
|
# header (a WebSocket handshake) must proxy straight through instead:
|
|
# WebSocket clients never follow the 301.
|
|
map $request_method:$http_upgrade:$loopback_canonical_host $loopback_origin {
|
|
default "";
|
|
"~*^GET::." $loopback_canonical_host;
|
|
"~*^HEAD::." $loopback_canonical_host;
|
|
}
|
|
|
|
server {
|
|
listen 2026;
|
|
listen [::]:2026;
|
|
server_name _;
|
|
|
|
# Canonicalize the loopback origin (#6156); see the maps above.
|
|
if ($loopback_origin != "") {
|
|
return 301 $scheme://$loopback_origin$request_uri;
|
|
}
|
|
|
|
# Keep the unified nginx endpoint same-origin by default. When split
|
|
# frontend/backend or port-forwarded deployments need browser CORS,
|
|
# configure the Gateway allowlist with GATEWAY_CORS_ORIGINS so CORS and
|
|
# CSRF origin checks stay aligned instead of approving every origin at
|
|
# the proxy layer.
|
|
|
|
# LangGraph-compatible API routes served by Gateway.
|
|
# Rewrites /api/langgraph/* to /api/* before proxying to Gateway.
|
|
location /api/langgraph/ {
|
|
rewrite ^/api/langgraph/(.*) /api/$1 break;
|
|
proxy_pass http://gateway;
|
|
proxy_http_version 1.1;
|
|
|
|
# Headers
|
|
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 $scheme;
|
|
proxy_set_header Connection '';
|
|
|
|
# SSE/Streaming support
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
proxy_set_header X-Accel-Buffering no;
|
|
|
|
# Long chat/text-prompt support (issue #3952): a sufficiently long
|
|
# pasted prompt otherwise exceeds nginx's default 1m
|
|
# client_max_body_size, or gets spooled to a temp file via
|
|
# proxy_request_buffering before reaching Gateway -- on a non-root
|
|
# local run that temp directory may not be writable, producing a
|
|
# raw nginx 500 instead of a graceful application error. Mirrors
|
|
# the uploads location's fix below (same mechanism, sized for text
|
|
# prompts rather than binary file uploads).
|
|
client_max_body_size 20M;
|
|
proxy_request_buffering off;
|
|
|
|
# Timeouts for long-running requests
|
|
proxy_connect_timeout 600s;
|
|
proxy_send_timeout 600s;
|
|
proxy_read_timeout 600s;
|
|
|
|
# Chunked transfer encoding
|
|
chunked_transfer_encoding on;
|
|
}
|
|
|
|
# Custom API: Models endpoint
|
|
location /api/models {
|
|
proxy_pass http://gateway;
|
|
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 $scheme;
|
|
|
|
# Disable buffering to avoid permission errors when nginx
|
|
# runs as a non-root user (e.g. local development).
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
}
|
|
|
|
# Custom API: Memory endpoint
|
|
location /api/memory {
|
|
proxy_pass http://gateway;
|
|
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 $scheme;
|
|
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
}
|
|
|
|
# Custom API: MCP configuration endpoint
|
|
location /api/mcp {
|
|
proxy_pass http://gateway;
|
|
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 $scheme;
|
|
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
}
|
|
|
|
# Admin-only local .skill archive upload from Settings.
|
|
location = /api/skills/install/upload {
|
|
proxy_pass http://gateway;
|
|
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 $scheme;
|
|
|
|
client_max_body_size 101M;
|
|
proxy_request_buffering off;
|
|
proxy_read_timeout 600s;
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
}
|
|
|
|
# Custom API: Skills configuration endpoint
|
|
location /api/skills {
|
|
proxy_pass http://gateway;
|
|
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 $scheme;
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
|
|
# Installing a .skill archive runs one LLM security scan per file, and a
|
|
# custom-skill edit or rollback runs one more; none sets its own timeout.
|
|
# The upload endpoint above already allows 600s for the same work.
|
|
proxy_read_timeout 600s;
|
|
}
|
|
|
|
# Custom API: Agents endpoint
|
|
location /api/agents {
|
|
proxy_pass http://gateway;
|
|
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 $scheme;
|
|
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
}
|
|
|
|
# Custom API: Uploads endpoint
|
|
location ~ ^/api/threads/[^/]+/uploads {
|
|
proxy_pass http://gateway;
|
|
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 $scheme;
|
|
|
|
# Large file upload support
|
|
client_max_body_size 100M;
|
|
proxy_request_buffering off;
|
|
|
|
# Disable response buffering to avoid permission errors
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
}
|
|
|
|
# Custom API: Project documents (multipart upload, promote-from-thread,
|
|
# attach-to-thread, content download, delete). Without its own location
|
|
# the upload fell through to /api/ and nginx's 1m default rejected
|
|
# anything larger with a 413 before Gateway saw it, although Gateway
|
|
# allows uploads.max_file_size (50 MiB by default) on this route. Same
|
|
# ceiling and streaming settings as the thread uploads route above; the
|
|
# read timeout keeps what /api/ already granted these routes.
|
|
location ~ ^/api/projects/[^/]+/documents {
|
|
proxy_pass http://gateway;
|
|
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 $scheme;
|
|
|
|
client_max_body_size 100M;
|
|
proxy_request_buffering off;
|
|
proxy_read_timeout 600s;
|
|
|
|
# Disable response buffering to avoid permission errors
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
}
|
|
|
|
# Live browser stream is a WebSocket upgrade. It must be matched before
|
|
# the generic /api/threads regex below (which omits Upgrade/Connection
|
|
# forwarding and would downgrade it to plain HTTP).
|
|
location ~ ^/api/threads/[^/]+/browser/stream {
|
|
proxy_pass http://gateway;
|
|
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 $scheme;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection 'upgrade';
|
|
proxy_cache_bypass $http_upgrade;
|
|
|
|
# Long-lived Live stream: keep the upgraded connection open.
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
proxy_connect_timeout 600s;
|
|
proxy_send_timeout 600s;
|
|
proxy_read_timeout 600s;
|
|
}
|
|
|
|
# Custom API: Other endpoints under /api/threads
|
|
location ~ ^/api/threads {
|
|
proxy_pass http://gateway;
|
|
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 $scheme;
|
|
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
|
|
# /compact and /suggestions hold the response open for a model call,
|
|
# /runs/wait for a whole run. nginx's 60s default would 504 them mid-work:
|
|
# the compaction still commits, and the waited run is cancelled.
|
|
proxy_read_timeout 600s;
|
|
}
|
|
|
|
# API Documentation: Swagger UI
|
|
location /api/docs {
|
|
proxy_pass http://gateway/docs ;
|
|
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 $scheme;
|
|
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
}
|
|
|
|
# API Documentation: ReDoc
|
|
location /api/redoc {
|
|
proxy_pass http://gateway/redoc;
|
|
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 $scheme;
|
|
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
}
|
|
|
|
# API Documentation: OpenAPI Schema
|
|
location /openapi.json {
|
|
proxy_pass http://gateway;
|
|
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 $scheme;
|
|
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
}
|
|
|
|
# Health check endpoint (gateway)
|
|
location /health {
|
|
proxy_pass http://gateway;
|
|
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 $scheme;
|
|
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
}
|
|
|
|
# Catch-all for any /api/* prefix not matched by a more specific block above.
|
|
# Covers the auth module (/api/v1/auth/login, /me, /change-password, ...),
|
|
# plus feedback / runs / token-usage routes that 2.0-rc added without
|
|
# updating this nginx config. Longest-prefix matching ensures the explicit
|
|
# blocks above (/api/models, /api/threads regex, /api/langgraph/, ...) still
|
|
# win for their paths — only truly unmatched /api/* requests land here.
|
|
location /api/ {
|
|
proxy_pass http://gateway;
|
|
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 $scheme;
|
|
|
|
# Auth endpoints set HttpOnly cookies — make sure nginx doesn't
|
|
# strip the Set-Cookie header from upstream responses.
|
|
proxy_pass_header Set-Cookie;
|
|
|
|
# Disable buffering to avoid permission errors when nginx
|
|
# runs as a non-root user (e.g. local development).
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
|
|
# /api/runs/wait blocks on a whole run and /api/input-polish on a model
|
|
# call. nginx's 60s default would 504 them mid-work, and the waited run is
|
|
# cancelled on the disconnect.
|
|
proxy_read_timeout 600s;
|
|
}
|
|
|
|
# All other requests go to frontend
|
|
location / {
|
|
proxy_pass http://frontend;
|
|
proxy_http_version 1.1;
|
|
|
|
# Headers
|
|
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 $scheme;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection $connection_upgrade;
|
|
proxy_cache_bypass $http_upgrade;
|
|
|
|
# Disable response buffering for the frontend. Without this,
|
|
# nginx tries to spool large upstream responses (e.g. Next.js
|
|
# static chunks) into ``proxy_temp_path``, which defaults to
|
|
# the system-owned ``/var/lib/nginx/proxy`` and fails with
|
|
# ``[crit] open() ... failed (13: Permission denied)`` when
|
|
# nginx is launched as a non-root user (every dev machine
|
|
# except production root containers). The symptom on the
|
|
# client side is ``ERR_INCOMPLETE_CHUNKED_ENCODING`` and
|
|
# ``ChunkLoadError`` partway through page hydration.
|
|
#
|
|
# Streaming the response straight through avoids the
|
|
# temp-file path entirely. The frontend already sets its
|
|
# own cache headers, so we don't lose anything from
|
|
# disabling nginx-side buffering.
|
|
proxy_buffering off;
|
|
proxy_request_buffering off;
|
|
|
|
# Timeouts
|
|
proxy_connect_timeout 600s;
|
|
proxy_send_timeout 600s;
|
|
proxy_read_timeout 600s;
|
|
}
|
|
}
|
|
}
|