Merge pull request #1707 from pikasTech/codex/1706-upstream-unavailable-copy
统一 Cloud Web 上游不可达错误文案
This commit is contained in:
@@ -6,6 +6,7 @@ import { promisify } from "node:util";
|
|||||||
|
|
||||||
import { isCloudWebSseRoute, proxyCloudApiRequest, upstreamRequestHeaders } from "./cloud-web-proxy.mjs";
|
import { isCloudWebSseRoute, proxyCloudApiRequest, upstreamRequestHeaders } from "./cloud-web-proxy.mjs";
|
||||||
import { cloudWebProxyRoutePolicy } from "./cloud-web-routes.mjs";
|
import { cloudWebProxyRoutePolicy } from "./cloud-web-routes.mjs";
|
||||||
|
import { UPSTREAM_UNAVAILABLE_ERROR_CODE, UPSTREAM_UNAVAILABLE_MESSAGE } from "./http.mjs";
|
||||||
const gzipAsync = promisify(gzip);
|
const gzipAsync = promisify(gzip);
|
||||||
const GZIP_MIN_BYTES = 1024;
|
const GZIP_MIN_BYTES = 1024;
|
||||||
const CLIENT_DISCONNECT_ERROR_CODES = new Set([
|
const CLIENT_DISCONNECT_ERROR_CODES = new Set([
|
||||||
@@ -443,12 +444,10 @@ export async function proxyCloudApi({ request, response, url, cloudApiBaseUrl, c
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const timedOut = error.timedOut === true || /timed out after \d+ms/iu.test(error.message);
|
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 category = timedOut ? "timeout" : "proxy";
|
||||||
const traceId = request.headers["x-trace-id"] || null;
|
const traceId = request.headers["x-trace-id"] || null;
|
||||||
const userMessage = timedOut
|
const userMessage = UPSTREAM_UNAVAILABLE_MESSAGE;
|
||||||
? "Code Agent 代理等待 cloud-api 超过 " + cloudApiProxyTimeoutMs + "ms;输入已保留,可稍后重试。"
|
|
||||||
: "Code Agent 代理暂时无法连接 cloud-api;输入已保留,可稍后重试。";
|
|
||||||
sendJson(response, 502, {
|
sendJson(response, 502, {
|
||||||
status: "failed",
|
status: "failed",
|
||||||
error: {
|
error: {
|
||||||
|
|||||||
@@ -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 () => {
|
test("cloud web proxies Admin Access write routes", async () => {
|
||||||
const upstreamRequests = [];
|
const upstreamRequests = [];
|
||||||
const upstream = createServer(async (request, response) => {
|
const upstream = createServer(async (request, response) => {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { buildMetadataFromEnv } from "../build-metadata.mjs";
|
|||||||
import { DEV_ENDPOINT, ENVIRONMENT_DEV } from "../protocol/index.mjs";
|
import { DEV_ENDPOINT, ENVIRONMENT_DEV } from "../protocol/index.mjs";
|
||||||
|
|
||||||
const DEFAULT_PROXY_TIMEOUT_MS = 180000;
|
const DEFAULT_PROXY_TIMEOUT_MS = 180000;
|
||||||
|
export const UPSTREAM_UNAVAILABLE_ERROR_CODE = "upstream_unavailable";
|
||||||
|
export const UPSTREAM_UNAVAILABLE_MESSAGE = "暂时无法连接上游。";
|
||||||
|
|
||||||
export function parsePort(value, fallback) {
|
export function parsePort(value, fallback) {
|
||||||
const parsed = Number.parseInt(value ?? "", 10);
|
const parsed = Number.parseInt(value ?? "", 10);
|
||||||
@@ -123,11 +125,9 @@ export function proxyHttpRequest({ request, response, upstream, timeoutMs = DEFA
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
settled = true;
|
settled = true;
|
||||||
const code = timedOut ? "proxy_timeout" : "upstream_unavailable";
|
const code = UPSTREAM_UNAVAILABLE_ERROR_CODE;
|
||||||
const category = timedOut ? "timeout" : "proxy";
|
const category = timedOut ? "timeout" : "proxy";
|
||||||
const userMessage = timedOut
|
const userMessage = UPSTREAM_UNAVAILABLE_MESSAGE;
|
||||||
? `Code Agent 代理等待上游超过 ${timeoutMs}ms;输入已保留,可稍后重试。`
|
|
||||||
: "Code Agent 代理暂时无法连接上游;输入已保留,可稍后重试。";
|
|
||||||
sendJson(response, 502, {
|
sendJson(response, 502, {
|
||||||
status: "failed",
|
status: "failed",
|
||||||
error: {
|
error: {
|
||||||
|
|||||||
@@ -187,18 +187,19 @@ test("dev entrypoint proxy returns 502 when hard timeout expires", async () => {
|
|||||||
assert.equal(response.status, 502);
|
assert.equal(response.status, 502);
|
||||||
const payload = await response.json();
|
const payload = await response.json();
|
||||||
assert.equal(payload.status, "failed");
|
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.layer, "proxy");
|
||||||
assert.equal(payload.error.category, "timeout");
|
assert.equal(payload.error.category, "timeout");
|
||||||
assert.equal(payload.error.retryable, true);
|
assert.equal(payload.error.retryable, true);
|
||||||
assert.equal(payload.error.timeoutMs, 50);
|
assert.equal(payload.error.timeoutMs, 50);
|
||||||
assert.equal(payload.error.traceId, "trc_dev-entrypoint-proxy-timeout");
|
assert.equal(payload.error.traceId, "trc_dev-entrypoint-proxy-timeout");
|
||||||
assert.match(payload.error.userMessage, /超过 50ms/u);
|
assert.equal(payload.error.userMessage, "暂时无法连接上游。");
|
||||||
assert.match(payload.error.message, /输入已保留/u);
|
assert.equal(payload.error.message, "暂时无法连接上游。");
|
||||||
assert.equal(payload.error.blocker.code, "proxy_timeout");
|
assert.equal(payload.error.blocker.code, "upstream_unavailable");
|
||||||
assert.equal(payload.error.blocker.retryable, true);
|
assert.equal(payload.error.blocker.retryable, true);
|
||||||
assert.equal(payload.traceId, "trc_dev-entrypoint-proxy-timeout");
|
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 {
|
} finally {
|
||||||
await close(proxy);
|
await close(proxy);
|
||||||
await close(upstream);
|
await close(upstream);
|
||||||
|
|||||||
@@ -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/specs") return json(response, 200, hwpodSpecsPayload());
|
||||||
if (path === "/v1/hwpod-node-ops") return json(response, 200, hwpodNodeOpsPayload());
|
if (path === "/v1/hwpod-node-ops") return json(response, 200, hwpodNodeOpsPayload());
|
||||||
if (path === "/v1/web-performance/summary") {
|
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-empty") return json(response, 200, webPerformanceEmptyPayload(url));
|
||||||
if (state.scenarioId === "performance-stale-window") return json(response, 200, webPerformanceStaleWindowPayload(url));
|
if (state.scenarioId === "performance-stale-window") return json(response, 200, webPerformanceStaleWindowPayload(url));
|
||||||
return json(response, 200, webPerformanceSummaryPayload(url));
|
return json(response, 200, webPerformanceSummaryPayload(url));
|
||||||
|
|||||||
@@ -144,7 +144,9 @@ test.describe("performance dashboard summary error", () => {
|
|||||||
|
|
||||||
await expect(page.getByRole("heading", { name: "性能监控" })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "性能监控" })).toBeVisible();
|
||||||
await expect(page.getByText("性能监控加载失败")).toBeVisible();
|
await expect(page.getByText("性能监控加载失败")).toBeVisible();
|
||||||
|
await expect(page.getByText("暂时无法连接上游。")).toBeVisible();
|
||||||
await expect(page.getByRole("button", { name: "重试" })).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);
|
await expect(page.locator(".performance-contract-item")).toHaveCount(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user