From 24a0f7b0320d14c66c6f5ceef02d02d408778f80 Mon Sep 17 00:00:00 2001 From: Ravi Tharuma Date: Mon, 16 Mar 2026 13:04:14 +0100 Subject: [PATCH 1/2] fix(runtime-fallback): extract status code from nested AI SDK errors AI SDK wraps HTTP status codes inside error.error.statusCode (e.g., AI_APICallError). The current extractStatusCode only checks the top level, missing these nested codes. This caused runtime-fallback to skip retryable errors like 400, 500, 504 because it couldn't find the status code. Fixes #2617 --- src/hooks/runtime-fallback/error-classifier.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/runtime-fallback/error-classifier.ts b/src/hooks/runtime-fallback/error-classifier.ts index c20aa8979..512ba734d 100644 --- a/src/hooks/runtime-fallback/error-classifier.ts +++ b/src/hooks/runtime-fallback/error-classifier.ts @@ -33,7 +33,7 @@ export function extractStatusCode(error: unknown, retryOnErrors?: number[]): num const errorObj = error as Record - const statusCode = errorObj.statusCode ?? errorObj.status ?? (errorObj.data as Record)?.statusCode + const statusCode = errorObj.statusCode ?? errorObj.status ?? (errorObj.data as Record)?.statusCode ?? (errorObj.error as Record)?.statusCode ?? (errorObj.cause as Record)?.statusCode if (typeof statusCode === "number") { return statusCode } From de66f1f397b85d60f59a6854185045fc368228d2 Mon Sep 17 00:00:00 2001 From: Ravi Tharuma Date: Mon, 16 Mar 2026 13:51:23 +0100 Subject: [PATCH 2/2] fix(runtime-fallback): prefer numeric status codes over non-numeric in extraction chain The nullish-coalescing chain could stop at a non-numeric value (e.g. status: "error"), preventing deeper nested numeric statusCode values from being reached. Switch to Array.find() with a type guard to always select the first numeric value. Adds 11 tests for extractStatusCode covering: top-level, nested (data/error/cause), non-numeric skip, fallback to regex, and precedence. Co-Authored-By: Claude Opus 4.6 --- .../runtime-fallback/error-classifier.test.ts | 71 ++++++++++++++++++- .../runtime-fallback/error-classifier.ts | 11 ++- 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/src/hooks/runtime-fallback/error-classifier.test.ts b/src/hooks/runtime-fallback/error-classifier.test.ts index 4719737fc..954e28758 100644 --- a/src/hooks/runtime-fallback/error-classifier.test.ts +++ b/src/hooks/runtime-fallback/error-classifier.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" -import { classifyErrorType, extractAutoRetrySignal, isRetryableError } from "./error-classifier" +import { classifyErrorType, extractAutoRetrySignal, extractStatusCode, isRetryableError } from "./error-classifier" describe("runtime-fallback error classifier", () => { test("detects cooling-down auto-retry status signals", () => { @@ -97,3 +97,72 @@ describe("runtime-fallback error classifier", () => { expect(signal).toBeUndefined() }) }) + +describe("extractStatusCode", () => { + test("extracts numeric statusCode from top-level", () => { + expect(extractStatusCode({ statusCode: 429 })).toBe(429) + }) + + test("extracts numeric status from top-level", () => { + expect(extractStatusCode({ status: 503 })).toBe(503) + }) + + test("extracts statusCode from nested data", () => { + expect(extractStatusCode({ data: { statusCode: 500 } })).toBe(500) + }) + + test("extracts statusCode from nested error", () => { + expect(extractStatusCode({ error: { statusCode: 502 } })).toBe(502) + }) + + test("extracts statusCode from nested cause", () => { + expect(extractStatusCode({ cause: { statusCode: 504 } })).toBe(504) + }) + + test("skips non-numeric status and finds deeper numeric statusCode", () => { + //#given — status is a string, but error.statusCode is numeric + const error = { + status: "error", + error: { statusCode: 429 }, + } + + //#when + const code = extractStatusCode(error) + + //#then + expect(code).toBe(429) + }) + + test("skips non-numeric statusCode string and finds numeric in cause", () => { + const error = { + statusCode: "UNKNOWN", + status: "failed", + cause: { statusCode: 503 }, + } + + expect(extractStatusCode(error)).toBe(503) + }) + + test("returns undefined when no numeric status exists", () => { + expect(extractStatusCode({ status: "error", message: "something broke" })).toBeUndefined() + }) + + test("returns undefined for null/undefined error", () => { + expect(extractStatusCode(null)).toBeUndefined() + expect(extractStatusCode(undefined)).toBeUndefined() + }) + + test("falls back to regex match in error message", () => { + const error = { message: "Request failed with status code 429" } + expect(extractStatusCode(error, [429, 503])).toBe(429) + }) + + test("prefers top-level numeric over nested numeric", () => { + const error = { + statusCode: 400, + error: { statusCode: 429 }, + cause: { statusCode: 503 }, + } + expect(extractStatusCode(error)).toBe(400) + }) +}) diff --git a/src/hooks/runtime-fallback/error-classifier.ts b/src/hooks/runtime-fallback/error-classifier.ts index 512ba734d..46d136b44 100644 --- a/src/hooks/runtime-fallback/error-classifier.ts +++ b/src/hooks/runtime-fallback/error-classifier.ts @@ -33,8 +33,15 @@ export function extractStatusCode(error: unknown, retryOnErrors?: number[]): num const errorObj = error as Record - const statusCode = errorObj.statusCode ?? errorObj.status ?? (errorObj.data as Record)?.statusCode ?? (errorObj.error as Record)?.statusCode ?? (errorObj.cause as Record)?.statusCode - if (typeof statusCode === "number") { + const statusCode = [ + errorObj.statusCode, + errorObj.status, + (errorObj.data as Record)?.statusCode, + (errorObj.error as Record)?.statusCode, + (errorObj.cause as Record)?.statusCode, + ].find((code): code is number => typeof code === "number") + + if (statusCode !== undefined) { return statusCode }