fix: preserve code agent short connection proxy headers

This commit is contained in:
Codex
2026-05-25 05:38:51 +00:00
parent 8f4be2e126
commit 76e8d90374
4 changed files with 39 additions and 2 deletions
@@ -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/<traceId>` 获取终态,并用 `/v1/agent/chat/trace/<traceId>` 刷新可视 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/<traceId>` 是终态摘要接口,不是完整 trace 下载接口。它可以携带压缩后的 `runnerTrace` 窗口用于当前 UI 刷新,但必须保留 `eventCount``lastEvent``providerTrace``threadId/sessionId` 和终态 reply/blocker;完整 trace 只能从 `/v1/agent/chat/trace/<traceId>`、复制 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 错误”并停止。
+10 -1
View File
@@ -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);
@@ -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");
+2
View File
@@ -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/);