From 76e8d90374992af6121f520fde7f668ec0075c10 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 25 May 2026 05:38:51 +0000 Subject: [PATCH] fix: preserve code agent short connection proxy headers --- docs/reference/code-agent-chat-readiness.md | 2 ++ internal/dev-entrypoint/cloud-web-proxy.mjs | 11 +++++++- .../dev-entrypoint/cloud-web-proxy.test.mjs | 26 ++++++++++++++++++- web/hwlab-cloud-web/scripts/check.mjs | 2 ++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/docs/reference/code-agent-chat-readiness.md b/docs/reference/code-agent-chat-readiness.md index 54d0f484..8bb344d8 100644 --- a/docs/reference/code-agent-chat-readiness.md +++ b/docs/reference/code-agent-chat-readiness.md @@ -108,6 +108,8 @@ Workbench trace 对已知 JSON-RPC gateway 响应应按普通 tool call 展示 Workbench 与 Code Agent 的用户请求必须是短连接 submit + 短连接 result/trace 轮询;浏览器或 cloud-web 不应持有一次长 HTTP 请求等待整个 Codex turn 结束。`POST /v1/agent/chat` 返回 `202` 后,前端轮询 `/v1/agent/chat/result/` 获取终态,并用 `/v1/agent/chat/trace/` 刷新可视 trace。 +cloud-web 同源代理必须把短连接语义原样转发给 cloud-api,至少包括 `Prefer: respond-async`、`X-HWLAB-Short-Connection` 和 `X-Trace-Id`。如果这些 header 在 cloud-web 层被过滤,cloud-api 会把同一个请求当成长同步请求处理,用户入口会表现为 16666 卡住或代理超时,而 16667 直连 cloud-api 正常。此类问题应先比对同一 trace 在 16666 与 16667 的 submit 行为,再修代理 header 透传,而不是调大前端等待时间。 + `/v1/agent/chat/result/` 是终态摘要接口,不是完整 trace 下载接口。它可以携带压缩后的 `runnerTrace` 窗口用于当前 UI 刷新,但必须保留 `eventCount`、`lastEvent`、`providerTrace`、`threadId/sessionId` 和终态 reply/blocker;完整 trace 只能从 `/v1/agent/chat/trace/`、复制 JSON 或下载 trace 入口取得。默认 result trace 窗口上限由 `HWLAB_CODE_AGENT_RESULT_TRACE_EVENT_LIMIT` 控制;不要把数百个大 chunk 原样塞进 result 响应,避免 cloud-web 代理层或浏览器 fetch 把“正常执行中的大响应”表现成 503、非 JSON 或空响应。 result 轮询的 408/425/429/5xx、浏览器 timeout、非 JSON 或空响应应按“可恢复传输抖动”处理:前端先拉取一次 trace 刷新活性,再带退避继续轮询,只有后端返回结构化 terminal blocker、真实终态失败,或 trace 按无新事件 idle timeout 超时,才向用户显示失败。只要 `/trace` 仍显示新事件或 `waitingFor` 仍在推进,就不能把一次 result poll 失败标成“Code Agent API 错误”并停止。 diff --git a/internal/dev-entrypoint/cloud-web-proxy.mjs b/internal/dev-entrypoint/cloud-web-proxy.mjs index 79d74d60..56cb0a7a 100644 --- a/internal/dev-entrypoint/cloud-web-proxy.mjs +++ b/internal/dev-entrypoint/cloud-web-proxy.mjs @@ -5,6 +5,15 @@ const STREAMING_HEADER_DEFAULTS = Object.freeze({ "x-accel-buffering": "no" }); +const FORWARDED_REQUEST_HEADERS = Object.freeze([ + "prefer", + "x-hwlab-short-connection", + "x-trace-id", + "x-request-id", + "x-actor-id", + "x-source-service-id" +]); + export function isCloudWebSseRoute(pathname) { return /^\/v1\/agent\/chat\/trace\/[^/]+\/stream$/u.test(String(pathname || "")); } @@ -39,7 +48,7 @@ export function upstreamRequestHeaders(request, body = "") { if (request.headers["content-type"] || hasBody) { headers["content-type"] = request.headers["content-type"] || "application/json"; } - for (const name of ["x-trace-id", "x-request-id", "x-actor-id", "x-source-service-id"]) { + for (const name of FORWARDED_REQUEST_HEADERS) { if (request.headers[name] !== undefined) headers[name] = request.headers[name]; } if (hasBody) headers["content-length"] = Buffer.byteLength(body); diff --git a/internal/dev-entrypoint/cloud-web-proxy.test.mjs b/internal/dev-entrypoint/cloud-web-proxy.test.mjs index 3fcd1e8b..26742114 100644 --- a/internal/dev-entrypoint/cloud-web-proxy.test.mjs +++ b/internal/dev-entrypoint/cloud-web-proxy.test.mjs @@ -4,9 +4,33 @@ import test from "node:test"; import { isCloudWebSseRoute, - proxyCloudApiRequest + proxyCloudApiRequest, + upstreamRequestHeaders } from "./cloud-web-proxy.mjs"; +test("cloud web proxy preserves Code Agent short-connection headers", () => { + const headers = upstreamRequestHeaders({ + headers: { + accept: "application/json", + "content-type": "application/json", + prefer: "respond-async", + "x-hwlab-short-connection": "1", + "x-trace-id": "trc_proxy_short_connection", + "x-request-id": "req_proxy_short_connection", + authorization: "Bearer must-not-forward" + } + }, "{\"message\":\"hello\"}"); + + assert.equal(headers.accept, "application/json"); + assert.equal(headers["content-type"], "application/json"); + assert.equal(headers.prefer, "respond-async"); + assert.equal(headers["x-hwlab-short-connection"], "1"); + assert.equal(headers["x-trace-id"], "trc_proxy_short_connection"); + assert.equal(headers["x-request-id"], "req_proxy_short_connection"); + assert.equal(headers.authorization, undefined); + assert.equal(headers["content-length"], Buffer.byteLength("{\"message\":\"hello\"}")); +}); + test("cloud web proxy streams SSE chunks without fixed content-length", async () => { const upstream = createServer((request, response) => { assert.equal(request.headers["x-trace-id"], "trc_stream_proxy"); diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs index 529649de..2dc85fe6 100644 --- a/web/hwlab-cloud-web/scripts/check.mjs +++ b/web/hwlab-cloud-web/scripts/check.mjs @@ -1165,6 +1165,8 @@ assert.match(artifactPublisher, /HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS/); assert.match(artifactPublisher, /cloudApiProxyTimeoutMs/); assert.match(cloudWebProxy, /setTimeout\(\(\) => \{/); assert.match(cloudWebProxy, /cloud api proxy timed out after/); +assert.match(cloudWebProxy, /"prefer"/); +assert.match(cloudWebProxy, /"x-hwlab-short-connection"/); assert.match(artifactPublisher, /readOnlyRpcMethods/); assert.match(artifactPublisher, /requestUpstream/); assert.match(artifactPublisher, /proxyCloudApiRequest/);