From 32a66f18b33f9cc656ee6c829040e26fbb6d993c Mon Sep 17 00:00:00 2001 From: lyon Date: Sat, 20 Jun 2026 14:26:44 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=BB=9F=E4=B8=80=E4=B8=8A=E6=B8=B8?= =?UTF-8?q?=E4=B8=8D=E5=8F=AF=E8=BE=BE=E9=94=99=E8=AF=AF=E6=96=87=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/dev-entrypoint/cloud-web-runtime.mjs | 7 ++-- .../dev-entrypoint/cloud-web-runtime.test.mjs | 39 +++++++++++++++++++ internal/dev-entrypoint/http.mjs | 8 ++-- internal/dev-entrypoint/http.test.mjs | 11 +++--- .../scripts/workbench-e2e-server.ts | 2 +- .../specs/admin-hwpod-performance.spec.ts | 2 + 6 files changed, 55 insertions(+), 14 deletions(-) diff --git a/internal/dev-entrypoint/cloud-web-runtime.mjs b/internal/dev-entrypoint/cloud-web-runtime.mjs index 2e1bae16..ef3bc12b 100644 --- a/internal/dev-entrypoint/cloud-web-runtime.mjs +++ b/internal/dev-entrypoint/cloud-web-runtime.mjs @@ -6,6 +6,7 @@ import { promisify } from "node:util"; import { isCloudWebSseRoute, proxyCloudApiRequest, upstreamRequestHeaders } from "./cloud-web-proxy.mjs"; import { cloudWebProxyRoutePolicy } from "./cloud-web-routes.mjs"; +import { UPSTREAM_UNAVAILABLE_ERROR_CODE, UPSTREAM_UNAVAILABLE_MESSAGE } from "./http.mjs"; const gzipAsync = promisify(gzip); const GZIP_MIN_BYTES = 1024; const CLIENT_DISCONNECT_ERROR_CODES = new Set([ @@ -443,12 +444,10 @@ export async function proxyCloudApi({ request, response, url, cloudApiBaseUrl, c return; } const timedOut = error.timedOut === true || /timed out after \d+ms/iu.test(error.message); - const code = timedOut ? "cloud_api_proxy_timeout" : "cloud_api_proxy_failed"; + const code = UPSTREAM_UNAVAILABLE_ERROR_CODE; const category = timedOut ? "timeout" : "proxy"; const traceId = request.headers["x-trace-id"] || null; - const userMessage = timedOut - ? "Code Agent 代理等待 cloud-api 超过 " + cloudApiProxyTimeoutMs + "ms;输入已保留,可稍后重试。" - : "Code Agent 代理暂时无法连接 cloud-api;输入已保留,可稍后重试。"; + const userMessage = UPSTREAM_UNAVAILABLE_MESSAGE; sendJson(response, 502, { status: "failed", error: { diff --git a/internal/dev-entrypoint/cloud-web-runtime.test.mjs b/internal/dev-entrypoint/cloud-web-runtime.test.mjs index 5d446655..aeae96ba 100644 --- a/internal/dev-entrypoint/cloud-web-runtime.test.mjs +++ b/internal/dev-entrypoint/cloud-web-runtime.test.mjs @@ -54,6 +54,45 @@ test("cloud web auth requests are proxied once", async () => { } }); +test("cloud web proxy reports a unified upstream unavailable error", async () => { + const cloudWeb = createCloudWebServer({ + serviceId: "hwlab-cloud-web", + cloudApiBaseUrl: "http://127.0.0.1:1", + cloudApiProxyTimeoutMs: 1000, + healthPayload: () => ({ status: "ok" }), + roots: [], + sendJson(response, statusCode, body) { + const payload = JSON.stringify(body); + response.writeHead(statusCode, { + "content-type": "application/json", + "content-length": Buffer.byteLength(payload) + }); + response.end(payload); + } + }); + await listen(cloudWeb); + + try { + const response = await fetch(`${serverUrl(cloudWeb)}/v1/web-performance/summary?window=15m`, { + headers: { accept: "application/json", "x-trace-id": "trc_proxy_upstream_unavailable" } + }); + assert.equal(response.status, 502); + const payload = await response.json(); + assert.equal(payload.status, "failed"); + assert.equal(payload.error.code, "upstream_unavailable"); + assert.equal(payload.error.layer, "proxy"); + assert.equal(payload.error.retryable, true); + assert.equal(payload.error.userMessage, "暂时无法连接上游。"); + assert.equal(payload.error.message, "暂时无法连接上游。"); + assert.equal(payload.error.blocker.code, "upstream_unavailable"); + assert.equal(payload.error.blocker.summary, "暂时无法连接上游。"); + assert.equal(payload.reason, "暂时无法连接上游。"); + assert.doesNotMatch(JSON.stringify(payload), /Code Agent|输入已保留|稍后重试/u); + } finally { + await close(cloudWeb); + } +}); + test("cloud web proxies Admin Access write routes", async () => { const upstreamRequests = []; const upstream = createServer(async (request, response) => { diff --git a/internal/dev-entrypoint/http.mjs b/internal/dev-entrypoint/http.mjs index 9c3d9987..9faf5b9b 100644 --- a/internal/dev-entrypoint/http.mjs +++ b/internal/dev-entrypoint/http.mjs @@ -4,6 +4,8 @@ import { buildMetadataFromEnv } from "../build-metadata.mjs"; import { DEV_ENDPOINT, ENVIRONMENT_DEV } from "../protocol/index.mjs"; const DEFAULT_PROXY_TIMEOUT_MS = 180000; +export const UPSTREAM_UNAVAILABLE_ERROR_CODE = "upstream_unavailable"; +export const UPSTREAM_UNAVAILABLE_MESSAGE = "暂时无法连接上游。"; export function parsePort(value, fallback) { const parsed = Number.parseInt(value ?? "", 10); @@ -123,11 +125,9 @@ export function proxyHttpRequest({ request, response, upstream, timeoutMs = DEFA return; } settled = true; - const code = timedOut ? "proxy_timeout" : "upstream_unavailable"; + const code = UPSTREAM_UNAVAILABLE_ERROR_CODE; const category = timedOut ? "timeout" : "proxy"; - const userMessage = timedOut - ? `Code Agent 代理等待上游超过 ${timeoutMs}ms;输入已保留,可稍后重试。` - : "Code Agent 代理暂时无法连接上游;输入已保留,可稍后重试。"; + const userMessage = UPSTREAM_UNAVAILABLE_MESSAGE; sendJson(response, 502, { status: "failed", error: { diff --git a/internal/dev-entrypoint/http.test.mjs b/internal/dev-entrypoint/http.test.mjs index 88853592..cf1d9c6a 100644 --- a/internal/dev-entrypoint/http.test.mjs +++ b/internal/dev-entrypoint/http.test.mjs @@ -187,18 +187,19 @@ test("dev entrypoint proxy returns 502 when hard timeout expires", async () => { assert.equal(response.status, 502); const payload = await response.json(); assert.equal(payload.status, "failed"); - assert.equal(payload.error.code, "proxy_timeout"); + assert.equal(payload.error.code, "upstream_unavailable"); assert.equal(payload.error.layer, "proxy"); assert.equal(payload.error.category, "timeout"); assert.equal(payload.error.retryable, true); assert.equal(payload.error.timeoutMs, 50); assert.equal(payload.error.traceId, "trc_dev-entrypoint-proxy-timeout"); - assert.match(payload.error.userMessage, /超过 50ms/u); - assert.match(payload.error.message, /输入已保留/u); - assert.equal(payload.error.blocker.code, "proxy_timeout"); + assert.equal(payload.error.userMessage, "暂时无法连接上游。"); + assert.equal(payload.error.message, "暂时无法连接上游。"); + assert.equal(payload.error.blocker.code, "upstream_unavailable"); assert.equal(payload.error.blocker.retryable, true); assert.equal(payload.traceId, "trc_dev-entrypoint-proxy-timeout"); - assert.match(payload.message, /Code Agent 代理等待上游超过 50ms/u); + assert.equal(payload.message, "暂时无法连接上游。"); + assert.doesNotMatch(JSON.stringify(payload), /Code Agent|输入已保留|稍后重试/u); } finally { await close(proxy); await close(upstream); diff --git a/web/hwlab-cloud-web/scripts/workbench-e2e-server.ts b/web/hwlab-cloud-web/scripts/workbench-e2e-server.ts index c1ffd6d4..7c539ac5 100644 --- a/web/hwlab-cloud-web/scripts/workbench-e2e-server.ts +++ b/web/hwlab-cloud-web/scripts/workbench-e2e-server.ts @@ -226,7 +226,7 @@ async function handleRequest(request: IncomingMessage, response: ServerResponse) if (path === "/v1/hwpod/specs") return json(response, 200, hwpodSpecsPayload()); if (path === "/v1/hwpod-node-ops") return json(response, 200, hwpodNodeOpsPayload()); if (path === "/v1/web-performance/summary") { - if (state.scenarioId === "performance-summary-error") return json(response, 503, { ok: false, status: 503, error: { code: "performance_summary_unavailable", message: "performance summary read model is unavailable" } }); + if (state.scenarioId === "performance-summary-error") return json(response, 502, { ok: false, status: 502, error: { code: "upstream_unavailable", message: "暂时无法连接上游。" } }); if (state.scenarioId === "performance-empty") return json(response, 200, webPerformanceEmptyPayload(url)); if (state.scenarioId === "performance-stale-window") return json(response, 200, webPerformanceStaleWindowPayload(url)); return json(response, 200, webPerformanceSummaryPayload(url)); diff --git a/web/hwlab-cloud-web/tests/workbench-e2e/specs/admin-hwpod-performance.spec.ts b/web/hwlab-cloud-web/tests/workbench-e2e/specs/admin-hwpod-performance.spec.ts index 978f837a..23a9ec80 100644 --- a/web/hwlab-cloud-web/tests/workbench-e2e/specs/admin-hwpod-performance.spec.ts +++ b/web/hwlab-cloud-web/tests/workbench-e2e/specs/admin-hwpod-performance.spec.ts @@ -144,7 +144,9 @@ test.describe("performance dashboard summary error", () => { await expect(page.getByRole("heading", { name: "性能监控" })).toBeVisible(); await expect(page.getByText("性能监控加载失败")).toBeVisible(); + await expect(page.getByText("暂时无法连接上游。")).toBeVisible(); await expect(page.getByRole("button", { name: "重试" })).toBeVisible(); + await expect(page.locator("body")).not.toContainText(/Code Agent|输入已保留|稍后重试/u); await expect(page.locator(".performance-contract-item")).toHaveCount(0); }); });