From 587c7524e58dfd21d49fade5fbc5550c4c3fb3c9 Mon Sep 17 00:00:00 2001 From: ruel225 Date: Fri, 21 Aug 2026 21:09:25 +0800 Subject: [PATCH] fix(normalization-core): preserve non-Error object causes with extra keys in formatErrorMessage (#126654) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(normalization-core): preserve non-Error object causes with extra keys The cause-chain branch of formatErrorMessage called only formatStatusAndCode(cause) with no stringifyUnknown fallback, while the top-level branch used formatStatusAndCode(value) ?? stringifyUnknown(value). formatStatusAndCode returns undefined for any object whose keys are not exactly status/code, so a non-Error object cause carrying extra keys (e.g. { statusCode: 429 } or { status: 503, code: "UNAVAILABLE", requestId: "abc" }) was silently dropped — appendCauseMessage(undefined) no-op'd and the loop broke, losing the diagnostic/retryable detail. Mirror the top-level branch: appendCauseMessage(formatStatusAndCode(cause) ?? stringifyUnknown(cause)). Behavior-neutral for causes that already render; restores the dropped detail for the asymmetric case. stringifyUnknown is a local helper in the same file. Closes #126652 Co-Authored-By: Claude * test(normalization-core): assert structured cause metadata --------- Co-authored-by: ruel225 Co-authored-by: Claude Co-authored-by: Altay --- .../normalization-core/src/error-coercion.test.ts | 14 +++++++++++++- packages/normalization-core/src/error-coercion.ts | 5 ++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/normalization-core/src/error-coercion.test.ts b/packages/normalization-core/src/error-coercion.test.ts index fc543fb6fafe..f2025c5bffaa 100644 --- a/packages/normalization-core/src/error-coercion.test.ts +++ b/packages/normalization-core/src/error-coercion.test.ts @@ -55,9 +55,21 @@ describe("formatErrorMessage", () => { expect(format(new Error("request failed", { cause: { status: 429 } }))).toBe( "request failed | status=429 code=unknown", ); + // A non-Error cause carrying recognized status/code fields alongside extra + // keys used to be dropped entirely: formatStatusAndCode returns undefined + // for any object with keys beyond status/code, and the cause-chain branch + // had no stringifyUnknown fallback (unlike the top-level branch). The + // structured detail now survives instead of being swallowed. expect(format(new Error("request failed", { cause: { statusCode: 429 } }))).toBe( - "request failed", + 'request failed | {"statusCode":429}', ); + expect( + format( + new Error("request failed", { + cause: { status: 503, code: "UNAVAILABLE", requestId: "abc" }, + }), + ), + ).toBe('request failed | {"status":503,"code":"UNAVAILABLE","requestId":"abc"}'); }); it("stringifies primitives and circular records without throwing", () => { diff --git a/packages/normalization-core/src/error-coercion.ts b/packages/normalization-core/src/error-coercion.ts index c96cc060e2b7..79c397f9011c 100644 --- a/packages/normalization-core/src/error-coercion.ts +++ b/packages/normalization-core/src/error-coercion.ts @@ -117,7 +117,10 @@ export function formatErrorMessage(value: unknown, options: FormatErrorMessageOp appendCauseMessage(cause); break; } else { - appendCauseMessage(formatStatusAndCode(cause)); + // Mirror the top-level branch: an object cause with keys beyond + // status/code makes formatStatusAndCode return undefined, so fall + // back to stringifyUnknown rather than dropping the cause entirely. + appendCauseMessage(formatStatusAndCode(cause) ?? stringifyUnknown(cause)); break; } }