From 1883ea8bee6c5d1cba76169b58b06af00d2ce6e9 Mon Sep 17 00:00:00 2001 From: erietz Date: Fri, 20 Feb 2026 17:19:29 -0600 Subject: [PATCH 1/3] use typeof for cross-realm function detection Replace `instanceof Function` with `typeof === 'function'` in `defaultBodySerializer` to fix form-urlencoded body serialization in environments with separate JavaScript realms (e.g., Jest with experimental VM modules). The instanceof check fails across realms because the Function constructor differs between realms, causing headers.get to not be recognized as a function even though it is one. --- packages/openapi-fetch/src/index.js | 2 +- .../openapi-fetch/test/common/request.test.ts | 55 ++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/packages/openapi-fetch/src/index.js b/packages/openapi-fetch/src/index.js index 4c7250fb0..9cb013480 100644 --- a/packages/openapi-fetch/src/index.js +++ b/packages/openapi-fetch/src/index.js @@ -627,7 +627,7 @@ export function defaultBodySerializer(body, headers) { } if (headers) { const contentType = - headers.get instanceof Function + typeof headers.get === "function" ? (headers.get("Content-Type") ?? headers.get("content-type")) : (headers["Content-Type"] ?? headers["content-type"]); if (contentType === "application/x-www-form-urlencoded") { diff --git a/packages/openapi-fetch/test/common/request.test.ts b/packages/openapi-fetch/test/common/request.test.ts index 6a836520e..e3a5a638b 100644 --- a/packages/openapi-fetch/test/common/request.test.ts +++ b/packages/openapi-fetch/test/common/request.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test, vi } from "vitest"; -import createClient, { type BodySerializer, type FetchOptions } from "../../src/index.js"; +import createClient, { type BodySerializer, defaultBodySerializer, type FetchOptions } from "../../src/index.js"; import { createObservedClient, headersToObj } from "../helpers.js"; import type { components, paths } from "./schemas/common.js"; @@ -278,6 +278,59 @@ describe("request", () => { expect(bodyUsed).toBe(true); expect(bodyText).toBe("key1=value1&key2=value2"); }); + + test("`application/x-www-form-urlencoded` body with Headers instance", async () => { + const { bodyUsed, bodyText } = await fireRequestAndGetBodyInformation({ + method: "POST", + fetchOptions: { + body: { key1: "value1", key2: "value2" }, + headers: new Headers({ "Content-Type": "application/x-www-form-urlencoded" }), + }, + }); + + expect(bodyUsed).toBe(true); + expect(bodyText).toBe("key1=value1&key2=value2"); + }); + }); + + describe("defaultBodySerializer", () => { + test("serializes to JSON by default", () => { + const body = { key1: "value1", key2: "value2" }; + expect(defaultBodySerializer(body, undefined)).toBe(JSON.stringify(body)); + }); + + test("serializes to URLSearchParams with plain object headers", () => { + const body = { key1: "value1", key2: "value2" }; + const headers = { "Content-Type": "application/x-www-form-urlencoded" }; + expect(defaultBodySerializer(body, headers)).toBe("key1=value1&key2=value2"); + }); + + test("serializes to URLSearchParams with Headers instance", () => { + const body = { key1: "value1", key2: "value2" }; + const headers = new Headers({ "Content-Type": "application/x-www-form-urlencoded" }); + expect(defaultBodySerializer(body, headers)).toBe("key1=value1&key2=value2"); + }); + + test("serializes to URLSearchParams with lowercase content-type header", () => { + const body = { key1: "value1", key2: "value2" }; + const headers = new Headers({ "content-type": "application/x-www-form-urlencoded" }); + expect(defaultBodySerializer(body, headers)).toBe("key1=value1&key2=value2"); + }); + + test("works with Headers-like object that has get method", () => { + // Simulates cross-realm scenario where instanceof Function fails but typeof works + const body = { key1: "value1", key2: "value2" }; + const headersLike = { + get: (name: string) => (name.toLowerCase() === "content-type" ? "application/x-www-form-urlencoded" : null), + }; + expect(defaultBodySerializer(body, headersLike as any)).toBe("key1=value1&key2=value2"); + }); + + test("returns FormData as-is", () => { + const formData = new FormData(); + formData.append("key1", "value1"); + expect(defaultBodySerializer(formData, undefined)).toBe(formData); + }); }); test("cookie header is preserved", async () => { From 43986b6dfef581c696df3b3adbfa6534f208b2f4 Mon Sep 17 00:00:00 2001 From: erietz Date: Mon, 23 Feb 2026 09:09:25 -0600 Subject: [PATCH 2/3] fix: type definition for defaultBodySerializer was missing a parameter --- packages/openapi-fetch/src/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/openapi-fetch/src/index.d.ts b/packages/openapi-fetch/src/index.d.ts index 5bca81075..83b603188 100644 --- a/packages/openapi-fetch/src/index.d.ts +++ b/packages/openapi-fetch/src/index.d.ts @@ -322,7 +322,7 @@ export declare function createQuerySerializer( export declare function defaultPathSerializer(pathname: string, pathParams: Record): string; /** Serialize body object to string */ -export declare function defaultBodySerializer(body: T): string; +export declare function defaultBodySerializer(body: T, headers?: HeadersOptions): T | string; /** Construct URL string from baseUrl and handle path and query params */ export declare function createFinalURL( From 89a599ee45154dd7161726b7b65d889e37af0ac0 Mon Sep 17 00:00:00 2001 From: erietz Date: Mon, 23 Feb 2026 09:18:08 -0600 Subject: [PATCH 3/3] docs: add changeset --- .changeset/khaki-clowns-fly.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/khaki-clowns-fly.md diff --git a/.changeset/khaki-clowns-fly.md b/.changeset/khaki-clowns-fly.md new file mode 100644 index 000000000..c08209888 --- /dev/null +++ b/.changeset/khaki-clowns-fly.md @@ -0,0 +1,5 @@ +--- +"openapi-fetch": patch +--- + +Replace `instanceof Function` with `typeof === 'function'` to fix form-urlencoded body serialization in environments with separate JavaScript realms (e.g., Jest with experimental VM modules). Also fixes missing `headers` parameter in the type definition for `defaultBodySerializer`.