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. */