From 3cfc9c58fdb6f3c396ed5a875710e5eacddc2127 Mon Sep 17 00:00:00 2001 From: Hyeonsang Cho Date: Thu, 17 Sep 2026 23:19:04 +0900 Subject: [PATCH] fix(nginx): allow model-bound /api/threads requests past 60 seconds (#5505) * fix(nginx): allow model-bound /api/threads requests past 60 seconds The browser calls /api/threads/* directly rather than through /api/langgraph/, and the generic `location ~ ^/api/threads` block set no proxy_read_timeout, so nginx's 60s default applied. /compact and /suggestions hold the response open for a whole model call, and /runs/wait for a whole run. Past 60s nginx returned 504 mid-work: the compaction still committed behind the failed request, and /runs/wait cancelled its run on the disconnect (on_disconnect defaults to cancel). Allow 600s on that location, matching /api/langgraph/, in all three copies of the nginx config: Docker, local dev, and the Helm ConfigMap. The regression test parses the active directive per config, so a missing, commented-out, lowered, or misplaced timeout fails. * docs(changelog): link the nginx /api/threads timeout entry to #5505 --------- Co-authored-by: Willem Jiang --- CHANGELOG.md | 10 +++++++ CHANGELOG_zh.md | 6 ++++ .../tests/test_nginx_langgraph_body_size.py | 29 +++++++++++++++++++ deploy/helm/deer-flow/README.md | 10 ++++--- .../deer-flow/templates/configmap-nginx.yaml | 4 +++ docker/nginx/nginx.conf | 4 +++ docker/nginx/nginx.local.conf | 5 ++++ 7 files changed, 64 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc5435f76..f934774ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -941,6 +941,15 @@ This release closes that milestone with **765 merged pull requests**. ### Fixed +- **nginx:** Stop thread routes that wait on a model call from failing at 60 + seconds. The browser calls `/api/threads/*` directly, and that location had + no `proxy_read_timeout`, so nginx's 60-second default applied while + `/api/langgraph/` allowed 600. A slow `/compact` returned 504 while Gateway + kept going and still saved the compaction, so the UI showed an error for + work that had been applied, inviting a retry that compacts it again. + `/suggestions` hit the same limit, and `/runs/wait` cancelled its run when + nginx dropped the connection. The Docker, local, and Helm configs now allow + 600 seconds on that location. ([#5505]) - **middleware:** Stop loop detection from cutting off an agent that pages through a file. `read_file` calls were keyed by 200-line buckets, so every read shorter than a bucket collapsed onto its neighbours: five sequential @@ -4245,3 +4254,4 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#5496]: https://github.com/bytedance/deer-flow/pull/5496 [#5501]: https://github.com/bytedance/deer-flow/pull/5501 [#5504]: https://github.com/bytedance/deer-flow/pull/5504 +[#5505]: https://github.com/bytedance/deer-flow/pull/5505 diff --git a/CHANGELOG_zh.md b/CHANGELOG_zh.md index fe74d954b..5bffa1443 100644 --- a/CHANGELOG_zh.md +++ b/CHANGELOG_zh.md @@ -727,6 +727,11 @@ ### 修复 +- **nginx:** 需要等待模型调用的线程路由不再在 60 秒时失败。浏览器直接调用 `/api/threads/*`, + 而该 location 没有设置 `proxy_read_timeout`,因此沿用 nginx 默认的 60 秒,而 `/api/langgraph/` + 允许 600 秒。较慢的 `/compact` 会返回 504,但 Gateway 仍会继续执行并保存压缩结果,于是 UI + 对已经生效的操作显示错误,诱使用户重试并再次压缩。`/suggestions` 也受同一限制,`/runs/wait` + 则会在 nginx 断开连接时取消其运行。Docker、本地开发与 Helm 配置现在都为该 location 允许 600 秒。([#5505]) - **中间件:** 循环检测不再中断正在分段读取文件的智能体。此前 `read_file` 调用按 200 行分桶作为 键,因此任何短于一个桶的读取都会与相邻读取塌缩到同一个键:连续五次 40 行读取会哈希成相同值并 触发硬停止,运行被迫给出最终答复并带上 `stop_reason=loop_capped`——而这恰恰是 `read_file` 自身 @@ -3477,3 +3482,4 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#5496]: https://github.com/bytedance/deer-flow/pull/5496 [#5501]: https://github.com/bytedance/deer-flow/pull/5501 [#5504]: https://github.com/bytedance/deer-flow/pull/5504 +[#5505]: https://github.com/bytedance/deer-flow/pull/5505 diff --git a/backend/tests/test_nginx_langgraph_body_size.py b/backend/tests/test_nginx_langgraph_body_size.py index 88b4f4080..1e133e7ea 100644 --- a/backend/tests/test_nginx_langgraph_body_size.py +++ b/backend/tests/test_nginx_langgraph_body_size.py @@ -52,6 +52,11 @@ _MAX_EXPECTED_BODY_SIZE_BYTES = 100 * 1024 * 1024 _SIZE_MULTIPLIERS = {"": 1, "k": 1024, "m": 1024**2, "g": 1024**3} +# The read timeout /api/langgraph/ already allows for a model-bound request. +_MIN_BLOCKING_READ_TIMEOUT_SECONDS = 600 + +_DURATION_MULTIPLIERS = {"": 1, "s": 1, "m": 60, "h": 3600} + def _read(path: str) -> str: return (REPO_ROOT / path).read_text(encoding="utf-8") @@ -88,6 +93,14 @@ def _parse_body_size_bytes(block: str) -> int: return int(value) * _SIZE_MULTIPLIERS[unit.lower()] +def _parse_read_timeout_seconds(block: str) -> int: + # Anchored to the start of a line so a commented-out directive does not count. + match = re.search(r"^\s*proxy_read_timeout\s+(\d+)([smh]?)\s*;", block, re.M) + assert match, "no active proxy_read_timeout directive" + value, unit = match.groups() + return int(value) * _DURATION_MULTIPLIERS[unit] + + @pytest.mark.parametrize("path", NGINX_CONFIGS) def test_langgraph_route_disables_request_buffering(path): content = _read(path) @@ -130,6 +143,22 @@ def test_uploads_route_still_has_its_own_body_size_settings(path): assert "proxy_request_buffering off;" in block +@pytest.mark.parametrize("path", NGINX_CONFIGS) +def test_threads_route_outlasts_blocking_gateway_calls(path): + """The browser calls ``/api/threads/*`` directly, not through + ``/api/langgraph/``. Routes such as ``/compact`` and ``/suggestions`` hold + the response open for a whole model call, and ``/runs/wait`` for a whole + run. Under nginx's 60s default the client gets a 504 mid-work: the + compaction still commits behind the failed request, and ``/runs/wait`` + cancels its run on the disconnect.""" + content = _read(path) + block = _extract_location_block(content, "~ ^/api/threads") + + timeout_seconds = _parse_read_timeout_seconds(block) + + assert timeout_seconds >= _MIN_BLOCKING_READ_TIMEOUT_SECONDS, f"{path}: the generic /api/threads location allows {timeout_seconds}s, expected at least {_MIN_BLOCKING_READ_TIMEOUT_SECONDS}s like /api/langgraph/" + + @pytest.mark.parametrize("path", NGINX_CONFIGS) def test_skills_upload_route_allows_archive_plus_multipart_framing(path): """The upload route must stream archives and allow slow validation.""" diff --git a/deploy/helm/deer-flow/README.md b/deploy/helm/deer-flow/README.md index d96b25756..9185ac96f 100644 --- a/deploy/helm/deer-flow/README.md +++ b/deploy/helm/deer-flow/README.md @@ -121,10 +121,12 @@ secrets: The default ingress annotations permit a 100 MiB local `.skill` archive plus multipart framing, stream request bodies without ingress buffering, and allow -up to 600 seconds for validation. If you replace `ingress.annotations`, -preserve equivalent size, streaming, and response-timeout settings for your -ingress controller or local skill uploads may fail before DeerFlow completes -the installation. +up to 600 seconds for a response, which skill validation and thread requests +that wait on a model call (such as `/compact`) both need. If you replace +`ingress.annotations`, preserve equivalent size, streaming, and +response-timeout settings for your ingress controller, or local skill uploads +may fail before DeerFlow completes the installation and those thread requests +may time out while Gateway is still working. Provide your model config under `config` (keep secrets as `$VAR` references — they resolve from the `secrets` map): diff --git a/deploy/helm/deer-flow/templates/configmap-nginx.yaml b/deploy/helm/deer-flow/templates/configmap-nginx.yaml index f9a982823..a6ff886cc 100644 --- a/deploy/helm/deer-flow/templates/configmap-nginx.yaml +++ b/deploy/helm/deer-flow/templates/configmap-nginx.yaml @@ -163,6 +163,10 @@ data: 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; + # /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; } location /docs { diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf index 35e181580..8c0780b00 100644 --- a/docker/nginx/nginx.conf +++ b/docker/nginx/nginx.conf @@ -225,6 +225,10 @@ http { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; + # /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 diff --git a/docker/nginx/nginx.local.conf b/docker/nginx/nginx.local.conf index f31e2c641..59f11df1c 100644 --- a/docker/nginx/nginx.local.conf +++ b/docker/nginx/nginx.local.conf @@ -234,6 +234,11 @@ http { 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