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 <willem.jiang@gmail.com>
This commit is contained in:
Hyeonsang Cho 2026-09-17 23:19:04 +09:00 committed by GitHub
parent 22ae3d0e95
commit 3cfc9c58fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 64 additions and 4 deletions

View File

@ -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

View File

@ -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

View File

@ -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."""

View File

@ -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):

View File

@ -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 {

View File

@ -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

View File

@ -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