Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/khaki-clowns-fly.md
Original file line number Diff line number Diff line change
@@ -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`.
2 changes: 1 addition & 1 deletion packages/openapi-fetch/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ export declare function createQuerySerializer<T = unknown>(
export declare function defaultPathSerializer(pathname: string, pathParams: Record<string, unknown>): string;

/** Serialize body object to string */
export declare function defaultBodySerializer<T>(body: T): string;
export declare function defaultBodySerializer<T>(body: T, headers?: HeadersOptions): T | string;

/** Construct URL string from baseUrl and handle path and query params */
export declare function createFinalURL<O>(
Expand Down
2 changes: 1 addition & 1 deletion packages/openapi-fetch/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
55 changes: 54 additions & 1 deletion packages/openapi-fetch/test/common/request.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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 () => {
Expand Down