From 63fba5d2fe1175d6a044ba5de21877b80f947cd7 Mon Sep 17 00:00:00 2001 From: nas <156536069+Nas01010101@users.noreply.github.com> Date: Sat, 6 Jun 2026 12:06:31 -0400 Subject: [PATCH] fix(cron): require HTTP context for server_error retries Narrow cron server_error retry classification so incidental 500-599 numbers in failure text no longer trigger retryable server_error. Genuine HTTP/status 5xx strings, canonical 5xx phrases, 5xx, and standalone terse codes still retry. Maintainer proof: focused cron retry tests, formatter/lint/diff checks, clean autoreview, Testbox-through-Crabbox check:changed tbx_01kteteqqrppbzgh560sybe0nk / Actions run 27066938422, and green PR CI on 6124f14850eb57f393f36ea5e9bfe7ed5a60257b. Fixes #90947. --- src/cron/retry-hint.test.ts | 34 ++++++++++++++++++++++++++++++++++ src/cron/retry-hint.ts | 13 ++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/cron/retry-hint.test.ts b/src/cron/retry-hint.test.ts index ff0324bbe84d..c2e4de25b1da 100644 --- a/src/cron/retry-hint.test.ts +++ b/src/cron/retry-hint.test.ts @@ -36,4 +36,38 @@ describe("resolveCronExecutionRetryHint", () => { retryable: false, }); }); + + it("does not classify bare 5xx-looking numbers as server_error", () => { + for (const message of [ + "context limit 512 exceeded", + "process exited with 503 lines of output", + "ENOENT: no such file '/var/run/app-540.sock'", + "killed worker pid 511 after deadline", + "assertion failed: expected 500 got 0", + "error 500 got 0", + "process exited with code 500", + ]) { + expect(resolveCronExecutionRetryHint(message, ["server_error"])).toEqual({ + retryable: false, + }); + } + }); + + it("classifies genuine HTTP 5xx errors as server_error", () => { + for (const message of [ + "HTTP 503 Service Unavailable", + "received status 500 from upstream", + "500 Internal Server Error", + "502 Bad Gateway", + "upstream returned 5xx", + "response code: 502", + "503", + "500", + ]) { + expect(resolveCronExecutionRetryHint(message, ["server_error"])).toEqual({ + retryable: true, + category: "server_error", + }); + } + }); }); diff --git a/src/cron/retry-hint.ts b/src/cron/retry-hint.ts index 253265d5210a..1dc097e189ac 100644 --- a/src/cron/retry-hint.ts +++ b/src/cron/retry-hint.ts @@ -7,6 +7,17 @@ export type CronRetryHint = { category?: CronRetryOn; }; +// A bare 5xx-looking number embedded in prose is not an HTTP server error: cron +// failure messages routinely contain such numbers ("context limit 512 exceeded", +// "exited with 503 lines", "pid 511 killed", a "...-540.sock" path), and +// /\b5\d{2}\b/ matched all of them, wrongly marking permanent failures retryable. +// Match a 5xx number only with HTTP/status context, a canonical 5xx phrase, or +// when it is the entire message (a terse "503"), so genuine +// "500 Internal Server Error" / "502 Bad Gateway" / "5xx" still classify while +// incidental numbers in longer messages do not. +const SERVER_ERROR_PATTERN = + /\b(?:https?|status(?:[ _]code)?|response(?:[ _]code)?|http(?:[ _]status)?)\b[\s:=#"']{0,4}5\d{2}\b|\b5\d{2}\b[\s:)\].,-]*(?:internal server error|server error|bad gateway|service unavailable|gateway time-?out)\b|\binternal server error\b|\bbad gateway\b|\bservice unavailable\b|\bgateway time-?out\b|\b5xx\b|^\s*5\d{2}\s*$/i; + const TRANSIENT_PATTERNS: Record = { rate_limit: /(rate[_ ]limit|too many requests|429|resource has been exhausted|cloudflare|tokens per day)/i, @@ -15,7 +26,7 @@ const TRANSIENT_PATTERNS: Record = { network: /(network|fetch failed|socket|econnreset|econnrefused|eai_again|enetdown|ehostunreach|ehostdown|enetreset|enetunreach|epipe)/i, timeout: /(timeout|etimedout)/i, - server_error: /\b5\d{2}\b/, + server_error: SERVER_ERROR_PATTERN, }; /** Classifies cron execution errors against the configured retryable transient categories. */