fix(nginx): allow model-bound /api/ and /api/skills requests past 60 seconds (#5524)

* fix(nginx): allow model-bound /api/ and /api/skills requests past 60 seconds

Two locations were left on nginx's 60s default while the routes behind
them wait on Gateway.

The /api/ catch-all carries the stateless POST /api/runs/wait, which
blocks on wait_for_run_completion and cancels its run when the client
disconnects, so a caller waiting on a longer run got a 504 and lost the
run; it also carries POST /api/input-polish, which waits for a one-shot
model call.

/api/skills carries POST /api/skills/install, which runs one LLM security
scan per file in the archive, and the custom-skill edit and rollback
routes, which run one more each. None of them sets an application-level
timeout, and only the sibling /api/skills/install/upload endpoint had
been given the longer timeout, so the same archive failed at 60s
depending on which endpoint installed it.

Allow 600s on both locations, matching /api/langgraph/ and /api/threads,
in all three copies of the nginx config. Each directive is pinned by its
own test that parses the active directive per config.

* docs(changelog): link the nginx /api/ and /api/skills timeout entry to #5524
This commit is contained in:
Hyeonsang Cho 2026-09-18 10:03:55 +09:00 committed by GitHub
parent 6d725f1ccb
commit 9f79ddf9b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 90 additions and 6 deletions

View File

@ -941,6 +941,19 @@ This release closes that milestone with **765 merged pull requests**.
### Fixed
- **nginx:** Extend the 600-second read timeout to the two remaining locations
whose routes wait on the Gateway, both left on nginx's 60-second default by
the thread-route fix. Behind the `/api/` catch-all, the stateless
`POST /api/runs/wait` blocks on the same run-completion wait and cancels its
run when the client disconnects, so an API consumer waiting on a run longer
than 60 seconds got a 504 *and* a cancelled run, and the composer's
`POST /api/input-polish` waits for a one-shot model call. Behind
`/api/skills`, installing a `.skill` archive runs one LLM security scan per
file in it, and a custom-skill edit or rollback runs one more; none of them
sets its own timeout, and only the sibling `/api/skills/install/upload`
endpoint had been given the longer timeout, so the same install through
`POST /api/skills/install` failed at 60 seconds. Applied to the Docker,
local, and Helm configs. ([#5524])
- **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
@ -4255,3 +4268,4 @@ with **180 merged pull requests** since the first 2.0 milestone tag.
[#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
[#5524]: https://github.com/bytedance/deer-flow/pull/5524

View File

@ -727,6 +727,14 @@
### 修复
- **nginx** 把 600 秒读取超时扩展到其余两个会等待 Gateway 的 location它们在线程路由的修复
之后仍沿用 nginx 默认的 60 秒。`/api/` 兜底 location 之后:无状态的 `POST /api/runs/wait`
阻塞在同一套运行完成等待上,并在客户端断开时取消该运行,因此等待超过 60 秒的 API 调用方会
同时收到 504 **并且**运行被取消;输入框的 `POST /api/input-polish` 则等待一次性模型调用。
`/api/skills` 之后:安装 `.skill` 压缩包会对其中每个文件各做一次 LLM 安全扫描,自定义技能的
编辑与回滚各再做一次,它们都没有自己的超时;此前只有同级的 `/api/skills/install/upload`
拿到了更长的超时,因此同样的安装经由 `POST /api/skills/install` 会在 60 秒失败。
Docker、本地开发与 Helm 配置均已应用。([#5524])
- **nginx** 需要等待模型调用的线程路由不再在 60 秒时失败。浏览器直接调用 `/api/threads/*`
而该 location 没有设置 `proxy_read_timeout`,因此沿用 nginx 默认的 60 秒,而 `/api/langgraph/`
允许 600 秒。较慢的 `/compact` 会返回 504但 Gateway 仍会继续执行并保存压缩结果,于是 UI
@ -3483,3 +3491,4 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子
[#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
[#5524]: https://github.com/bytedance/deer-flow/pull/5524

View File

@ -159,6 +159,22 @@ def test_threads_route_outlasts_blocking_gateway_calls(path):
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_api_catchall_outlasts_blocking_gateway_calls(path):
"""The routes that wait on Gateway are not all under ``/api/threads``.
The stateless ``POST /api/runs/wait`` blocks on the same
``wait_for_run_completion`` and cancels its run when the client
disconnects, and the composer's ``POST /api/input-polish`` waits for a
one-shot model call. Both fall through to this catch-all, so it needs the
same read timeout as the thread routes."""
content = _read(path)
block = _extract_location_block(content, "/api/")
timeout_seconds = _parse_read_timeout_seconds(block)
assert timeout_seconds >= _MIN_BLOCKING_READ_TIMEOUT_SECONDS, f"{path}: the /api/ catch-all allows {timeout_seconds}s, expected at least {_MIN_BLOCKING_READ_TIMEOUT_SECONDS}s like /api/langgraph/ and /api/threads"
@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."""
@ -170,6 +186,21 @@ def test_skills_upload_route_allows_archive_plus_multipart_framing(path):
assert "proxy_read_timeout 600s;" in block
@pytest.mark.parametrize("path", NGINX_CONFIGS)
def test_skills_prefix_outlasts_the_llm_security_scan(path):
"""Installing a ``.skill`` archive runs one LLM security scan per file in
it, and editing or rolling back a custom skill runs one more; none of them
sets an application-level timeout. The upload endpoint above already gets
600s, but ``POST /api/skills/install`` and the custom-skill writes land
here, so this prefix needs it too."""
content = _read(path)
block = _extract_location_block(content, "/api/skills")
timeout_seconds = _parse_read_timeout_seconds(block)
assert timeout_seconds >= _MIN_BLOCKING_READ_TIMEOUT_SECONDS, f"{path}: /api/skills allows {timeout_seconds}s, expected at least {_MIN_BLOCKING_READ_TIMEOUT_SECONDS}s like its own /api/skills/install/upload endpoint"
@pytest.mark.parametrize("path", NGINX_CONFIGS)
def test_skills_prefix_keeps_default_request_body_policy(path):
"""Large bodies must be allowed only on the admin upload endpoint."""

View File

@ -121,12 +121,14 @@ 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 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.
up to 600 seconds for a response, which the API requests that wait on a model
call or a whole run all need — skill install and custom-skill edits (each file
is scanned by an LLM), `/api/threads/{id}/compact`, `/api/input-polish`, and
`/api/runs/wait`. 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 requests may time out while Gateway is still working — for
`/api/runs/wait` the disconnect also cancels the run.
Provide your model config under `config` (keep secrets as `$VAR` references —
they resolve from the `secrets` map):

View File

@ -133,6 +133,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;
# 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;
}
location /api/agents {
@ -227,6 +231,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;
# /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;
}
# Everything else -> frontend (with WebSocket upgrade for HMR/sockets).

View File

@ -167,6 +167,11 @@ http {
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;
# 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
@ -301,6 +306,11 @@ http {
# Disable buffering to avoid permission errors when nginx
# runs as a non-root user (e.g. local development).
# /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

View File

@ -168,6 +168,11 @@ http {
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
@ -315,6 +320,11 @@ http {
# 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