From 5c988e74d6b4b303117686aa597e18e7d5af9c9e Mon Sep 17 00:00:00 2001 From: MohammadYusif Date: Thu, 28 May 2026 03:56:59 +0300 Subject: [PATCH] fix(utils): move instanceof checks before value.buffer in isJSONSerializable (#580) The `instanceof FormData` and `instanceof URLSearchParams` guards were placed after `value.buffer`, meaning any object that happens to have a `buffer` property would be rejected before the explicit type-based exclusion could run. More critically, accessing `value.buffer` on certain host objects can throw before reaching those guards. Moving the `instanceof` checks first ensures that `FormData` and `URLSearchParams` instances are always rejected early, regardless of whether they expose a `buffer` property, and avoids any risk of a thrown accessor on non-plain objects. Also adds direct unit tests for `isJSONSerializable` covering FormData, URLSearchParams (both empty and non-empty), TypedArray, plain objects, arrays, objects with toJSON, and undefined. --- src/utils.ts | 6 +++--- test/index.test.ts | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index a773431b..f8d3afdf 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -28,14 +28,14 @@ export function isJSONSerializable(value: any): boolean { if (Array.isArray(value)) { return true; } - if (value.buffer) { - return false; - } // `FormData` and `URLSearchParams` should't have a `toJSON` method, // but Bun adds it, which is non-standard. if (value instanceof FormData || value instanceof URLSearchParams) { return false; } + if (value.buffer) { + return false; + } return ( (value.constructor && value.constructor.name === "Object") || typeof value.toJSON === "function" diff --git a/test/index.test.ts b/test/index.test.ts index 5ac20b07..120edc86 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -10,6 +10,7 @@ import { import { Readable } from "node:stream"; import { H3, HTTPError, readBody, serve } from "h3"; import { $fetch } from "../src/index.ts"; +import { isJSONSerializable } from "../src/utils.ts"; describe("ofetch", () => { let listener: ReturnType; @@ -525,3 +526,47 @@ describe("ofetch", () => { }); }); }); + +describe("isJSONSerializable", () => { + it("returns false for FormData (non-empty)", () => { + const fd = new FormData(); + fd.append("key", "value"); + expect(isJSONSerializable(fd)).toBe(false); + }); + + it("returns false for empty FormData", () => { + expect(isJSONSerializable(new FormData())).toBe(false); + }); + + it("returns false for URLSearchParams (non-empty)", () => { + expect(isJSONSerializable(new URLSearchParams({ foo: "bar" }))).toBe(false); + }); + + it("returns false for empty URLSearchParams", () => { + expect(isJSONSerializable(new URLSearchParams())).toBe(false); + }); + + it("returns false for ArrayBuffer", () => { + expect(isJSONSerializable(new ArrayBuffer(8))).toBe(false); + }); + + it("returns false for Uint8Array", () => { + expect(isJSONSerializable(new Uint8Array([1, 2, 3]))).toBe(false); + }); + + it("returns true for plain objects", () => { + expect(isJSONSerializable({ a: 1 })).toBe(true); + }); + + it("returns true for arrays", () => { + expect(isJSONSerializable([1, 2, 3])).toBe(true); + }); + + it("returns true for objects with toJSON method", () => { + expect(isJSONSerializable({ toJSON: () => ({}) })).toBe(true); + }); + + it("returns false for undefined", () => { + expect(isJSONSerializable(undefined)).toBe(false); + }); +});