From b1f83d0bb2de14c83356ec55e4250bf8fc34becf Mon Sep 17 00:00:00 2001 From: robobun Date: Mon, 20 Oct 2025 19:46:22 -0700 Subject: [PATCH] fix: Response.json() throws TypeError for non-JSON serializable top-level values (#21258) Co-authored-by: Claude Bot Co-authored-by: Claude Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Meghan Denny --- src/bun.js/webcore/Response.zig | 11 ++++++++++ test/js/web/fetch/fetch.test.ts | 24 ++++++++++++++++++++++ test/regression/issue/21257.test.ts | 32 +++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 test/regression/issue/21257.test.ts diff --git a/src/bun.js/webcore/Response.zig b/src/bun.js/webcore/Response.zig index 78232d9348..0dde39911d 100644 --- a/src/bun.js/webcore/Response.zig +++ b/src/bun.js/webcore/Response.zig @@ -515,6 +515,17 @@ pub fn constructJSON( const json_value = args.nextEat() orelse jsc.JSValue.zero; if (@intFromEnum(json_value) != 0) { + // Validate top-level values that are not JSON serializable (Node.js compatibility) + if (json_value.isUndefined() or json_value.isSymbol() or json_value.jsType() == .JSFunction) { + const err = globalThis.createTypeErrorInstance("Value is not JSON serializable", .{}); + return globalThis.throwValue(err); + } + + // BigInt has a different error message to match Node.js exactly + if (json_value.isBigInt()) { + const err = globalThis.createTypeErrorInstance("Do not know how to serialize a BigInt", .{}); + return globalThis.throwValue(err); + } var str = bun.String.empty; // calling JSON.stringify on an empty string adds extra quotes // so this is correct diff --git a/test/js/web/fetch/fetch.test.ts b/test/js/web/fetch/fetch.test.ts index 3844afac26..37a1baad6f 100644 --- a/test/js/web/fetch/fetch.test.ts +++ b/test/js/web/fetch/fetch.test.ts @@ -1172,6 +1172,30 @@ describe("Response", () => { expect(response.headers.get("x-hello")).toBe("world"); expect(response.status).toBe(408); }); + + it("throws TypeError for non-JSON serializable top-level values (Node.js compatibility)", () => { + // Symbol, Function, and undefined should throw "Value is not JSON serializable" + expect(() => Response.json(Symbol("test"))).toThrow("Value is not JSON serializable"); + expect(() => Response.json(function () {})).toThrow("Value is not JSON serializable"); + expect(() => Response.json(undefined)).toThrow("Value is not JSON serializable"); + + // These should not throw (valid values) + expect(() => Response.json(null)).not.toThrow(); + expect(() => Response.json({})).not.toThrow(); + expect(() => Response.json("string")).not.toThrow(); + expect(() => Response.json(123)).not.toThrow(); + expect(() => Response.json(true)).not.toThrow(); + expect(() => Response.json([1, 2, 3])).not.toThrow(); + + // Objects containing non-serializable values should not throw at top-level + // (they get filtered out by JSON.stringify) + expect(() => Response.json({ symbol: Symbol("test") })).not.toThrow(); + expect(() => Response.json({ func: function () {} })).not.toThrow(); + expect(() => Response.json({ undef: undefined })).not.toThrow(); + + // BigInt should throw with Node.js compatible error message + expect(() => Response.json(123n)).toThrow("Do not know how to serialize a BigInt"); + }); }); describe("Response.redirect", () => { it("works", () => { diff --git a/test/regression/issue/21257.test.ts b/test/regression/issue/21257.test.ts new file mode 100644 index 0000000000..c3b15c8901 --- /dev/null +++ b/test/regression/issue/21257.test.ts @@ -0,0 +1,32 @@ +// Regression test for GitHub Issue #21257 +// https://github.com/oven-sh/bun/issues/21257 +// `Response.json()` should throw with top level value of `function` `symbol` `undefined` (node compatibility) + +import { expect, test } from "bun:test"; + +test("Response.json() throws TypeError for non-JSON serializable top-level values", () => { + // These should throw "Value is not JSON serializable" + expect(() => Response.json(Symbol("test"))).toThrow("Value is not JSON serializable"); + expect(() => Response.json(function testFunc() {})).toThrow("Value is not JSON serializable"); + expect(() => Response.json(undefined)).toThrow("Value is not JSON serializable"); +}); + +test("Response.json() works correctly with valid values", () => { + // These should not throw + expect(() => Response.json(null)).not.toThrow(); + expect(() => Response.json({})).not.toThrow(); + expect(() => Response.json("string")).not.toThrow(); + expect(() => Response.json(123)).not.toThrow(); + expect(() => Response.json(true)).not.toThrow(); + expect(() => Response.json([1, 2, 3])).not.toThrow(); + + // Objects containing non-serializable values should not throw at top-level + expect(() => Response.json({ symbol: Symbol("test") })).not.toThrow(); + expect(() => Response.json({ func: function () {} })).not.toThrow(); + expect(() => Response.json({ undef: undefined })).not.toThrow(); +}); + +test("Response.json() BigInt error matches Node.js", () => { + // BigInt should throw with Node.js compatible error message + expect(() => Response.json(123n)).toThrow("Do not know how to serialize a BigInt"); +});