fix(qa): retain Telegram proxy failure facts (#163598)

This commit is contained in:
Dallin Romney 2026-10-02 08:36:47 -07:00 • committed by GitHub
parent 93e27de58f
commit 35126bbe59
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 81 additions and 3 deletions

View file

@ -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" });
}

View file

@ -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;