mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
fix(qa): retain Telegram proxy failure facts (#163598)
This commit is contained in:
parent
93e27de58f
commit
35126bbe59
2 changed files with 81 additions and 3 deletions
|
|
@ -16,6 +16,8 @@ const HOP_BY_HOP_HEADERS = new Set([
|
|||
"transfer-encoding",
|
||||
"upgrade",
|
||||
]);
|
||||
const TELEGRAM_PROXY_ERROR_CODE =
|
||||
/^(?:ABORT_ERR|EAI_AGAIN|E(?:CONNREFUSED|CONNRESET|HOSTUNREACH|NETUNREACH|NOTFOUND|PIPE|TIMEDOUT)|UND_ERR_(?:ABORTED|BODY_TIMEOUT|CONNECT_TIMEOUT|HEADERS_TIMEOUT|SOCKET))$/u;
|
||||
|
||||
export function telegramTestApiPath(pathname) {
|
||||
const match = pathname.match(/^((?:\/file)?\/bot[^/]+)(\/.*)$/u);
|
||||
|
|
@ -47,6 +49,32 @@ function telegramApiMethod(pathname) {
|
|||
return pathname.match(/^\/bot[^/]+\/([^/?]+)/u)?.[1];
|
||||
}
|
||||
|
||||
function reportProxyFailure(error, phase, method) {
|
||||
let current = error;
|
||||
let errorCode;
|
||||
for (let depth = 0; depth < 4 && current && typeof current === "object"; depth += 1) {
|
||||
if (typeof current.code === "string" && TELEGRAM_PROXY_ERROR_CODE.test(current.code)) {
|
||||
errorCode = current.code;
|
||||
break;
|
||||
}
|
||||
current = current.cause;
|
||||
}
|
||||
// Error messages, URLs, and arbitrary names/codes can contain bot tokens. Emit only
|
||||
// fixed phases and allowlisted atoms so retained QA logs remain safe to publish.
|
||||
const safeMethod =
|
||||
typeof method === "string" && /^[A-Za-z][A-Za-z0-9_]{0,63}$/u.test(method) ? method : "unknown";
|
||||
console.error(
|
||||
JSON.stringify({
|
||||
event: "telegram_test_api_proxy_failure",
|
||||
phase,
|
||||
method: safeMethod,
|
||||
errorClass:
|
||||
error instanceof TypeError ? "TypeError" : error instanceof Error ? "Error" : "unknown",
|
||||
errorCode: errorCode ?? "unknown",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function drainTelegramTestUpdates(apiRoot, token) {
|
||||
let offset = 0;
|
||||
for (;;) {
|
||||
|
|
@ -137,6 +165,8 @@ export async function startTelegramTestApiProxy({
|
|||
});
|
||||
async function handleRequest(request, response) {
|
||||
let logged;
|
||||
let method;
|
||||
let failurePhase = "request";
|
||||
const recordDoneAt = () => {
|
||||
if (logged) {
|
||||
logged.doneAt ??= Date.now();
|
||||
|
|
@ -148,12 +178,14 @@ export async function startTelegramTestApiProxy({
|
|||
request.once("aborted", abortUpstream);
|
||||
response.once("close", abortUpstream);
|
||||
try {
|
||||
failurePhase = "lease";
|
||||
assertLeaseHealthy();
|
||||
failurePhase = "request";
|
||||
const incoming = new URL(request.url || "/", `http://${host}`);
|
||||
const upstreamUrl = new URL(upstream);
|
||||
upstreamUrl.pathname = telegramTestApiPath(incoming.pathname);
|
||||
upstreamUrl.search = incoming.search;
|
||||
const method = telegramApiMethod(incoming.pathname);
|
||||
method = telegramApiMethod(incoming.pathname);
|
||||
const loggedMethod =
|
||||
method ?? (incoming.pathname.startsWith("/file/bot") ? "file" : undefined);
|
||||
const ordinal = (methodOrdinals.get(method) ?? 0) + 1;
|
||||
|
|
@ -202,6 +234,7 @@ export async function startTelegramTestApiProxy({
|
|||
if (matches && rejection.skip > 0) {
|
||||
rejection.skip -= 1;
|
||||
} else if (matches) {
|
||||
failurePhase = "lease";
|
||||
assertLeaseHealthy();
|
||||
rejection.times -= 1;
|
||||
if (rejection.times <= 0) requestRejection = undefined;
|
||||
|
|
@ -242,6 +275,7 @@ export async function startTelegramTestApiProxy({
|
|||
return;
|
||||
}
|
||||
}
|
||||
failurePhase = "upstream-fetch";
|
||||
const result = await fetchImpl(upstreamUrl, {
|
||||
method: request.method,
|
||||
headers: requestHeaders(request.headers),
|
||||
|
|
@ -249,7 +283,9 @@ export async function startTelegramTestApiProxy({
|
|||
signal: upstreamController.signal,
|
||||
});
|
||||
if (logged) logged.status = result.status;
|
||||
failurePhase = "lease";
|
||||
assertLeaseHealthy();
|
||||
failurePhase = "response-stream";
|
||||
const hold = method ? claimResponseHold(method, ordinal) : undefined;
|
||||
if (method === "getUpdates") {
|
||||
try {
|
||||
|
|
@ -278,13 +314,17 @@ export async function startTelegramTestApiProxy({
|
|||
} finally {
|
||||
clearInterval(heartbeat);
|
||||
}
|
||||
failurePhase = "lease";
|
||||
assertLeaseHealthy();
|
||||
failurePhase = "response-stream";
|
||||
heldResponse.event.releasedAt = Date.now();
|
||||
heldResponse = undefined;
|
||||
response.end(body);
|
||||
return;
|
||||
}
|
||||
failurePhase = "lease";
|
||||
assertLeaseHealthy();
|
||||
failurePhase = "response-stream";
|
||||
response.writeHead(result.status, responseHeaders(result.headers));
|
||||
if (!result.body) {
|
||||
response.end();
|
||||
|
|
@ -310,8 +350,9 @@ export async function startTelegramTestApiProxy({
|
|||
response.once("close", finished);
|
||||
readable.pipe(response);
|
||||
});
|
||||
} catch {
|
||||
} catch (error) {
|
||||
recordDoneAt();
|
||||
reportProxyFailure(error, failurePhase, method);
|
||||
if (!response.headersSent) {
|
||||
response.writeHead(502, { "content-type": "application/json" });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -363,6 +363,7 @@ test("getFile timing ends when the streamed response finishes", async (t) => {
|
|||
});
|
||||
|
||||
test("records completion timing on upstream and response-stream errors", async (t) => {
|
||||
t.mock.method(console, "error", () => {});
|
||||
for (const [expectedStatus, fetchImpl] of [
|
||||
[
|
||||
502,
|
||||
|
|
@ -392,7 +393,42 @@ test("records completion timing on upstream and response-stream errors", async (
|
|||
}
|
||||
});
|
||||
|
||||
test("proxy close aborts the in-flight Test Server request", async () => {
|
||||
test("reports upstream failures without exposing exception details or bot tokens", async (t) => {
|
||||
const privateDetail = "synthetic-private-upstream-detail";
|
||||
const cause = Object.assign(new Error(privateDetail), { code: "ECONNRESET" });
|
||||
const diagnostic = t.mock.method(console, "error", () => {});
|
||||
const proxy = await startTelegramTestApiProxy({
|
||||
fetchImpl: async () => {
|
||||
throw new TypeError(privateDetail, { cause });
|
||||
},
|
||||
});
|
||||
t.after(() => proxy.close());
|
||||
|
||||
const response = await fetch(`${proxy.apiRoot}/bot123:ABC/deleteWebhook`, {
|
||||
method: "POST",
|
||||
body: "{}",
|
||||
});
|
||||
|
||||
assert.equal(response.status, 502);
|
||||
assert.deepEqual(await response.json(), {
|
||||
ok: false,
|
||||
description: "Telegram Test Server proxy failed.",
|
||||
});
|
||||
assert.equal(diagnostic.mock.calls.length, 1);
|
||||
const line = diagnostic.mock.calls[0]?.arguments[0];
|
||||
assert.equal(typeof line, "string");
|
||||
assert.deepEqual(JSON.parse(line), {
|
||||
event: "telegram_test_api_proxy_failure",
|
||||
phase: "upstream-fetch",
|
||||
method: "deleteWebhook",
|
||||
errorClass: "TypeError",
|
||||
errorCode: "ECONNRESET",
|
||||
});
|
||||
assert.doesNotMatch(line, /123:ABC|synthetic-private/u);
|
||||
});
|
||||
|
||||
test("proxy close aborts the in-flight Test Server request", async (t) => {
|
||||
t.mock.method(console, "error", () => {});
|
||||
let upstreamStarted;
|
||||
let upstreamAborted = false;
|
||||
const started = new Promise((resolve) => {
|
||||
|
|
@ -424,6 +460,7 @@ test("proxy close aborts the in-flight Test Server request", async () => {
|
|||
});
|
||||
|
||||
test("lease revocation blocks every later Bot API request", async (t) => {
|
||||
t.mock.method(console, "error", () => {});
|
||||
const leaseError = new Error("lease revoked");
|
||||
let healthy = true;
|
||||
let revoke;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue