From bd35e6bad4128c70777aed3bad77569c46547fa0 Mon Sep 17 00:00:00 2001 From: Carson Bruce Date: Wed, 1 Oct 2025 15:27:44 -0400 Subject: [PATCH 1/2] feat(openapi-fetch): Enable per request middleware --- .changeset/nice-parts-poke.md | 5 + packages/openapi-fetch/src/index.d.ts | 1 + packages/openapi-fetch/src/index.js | 33 +++-- .../test/middleware/middleware.test.ts | 115 ++++++++++++++++++ 4 files changed, 146 insertions(+), 8 deletions(-) create mode 100644 .changeset/nice-parts-poke.md diff --git a/.changeset/nice-parts-poke.md b/.changeset/nice-parts-poke.md new file mode 100644 index 000000000..4ec4f68df --- /dev/null +++ b/.changeset/nice-parts-poke.md @@ -0,0 +1,5 @@ +--- +"openapi-fetch": minor +--- + +Enable request level middlewares option diff --git a/packages/openapi-fetch/src/index.d.ts b/packages/openapi-fetch/src/index.d.ts index 79dec3d77..387a5642f 100644 --- a/packages/openapi-fetch/src/index.d.ts +++ b/packages/openapi-fetch/src/index.d.ts @@ -120,6 +120,7 @@ export type RequestOptions = ParamsOption & parseAs?: ParseAs; fetch?: ClientOptions["fetch"]; headers?: HeadersOptions; + middleware?: Middleware[]; }; export type MergedOptions = { diff --git a/packages/openapi-fetch/src/index.js b/packages/openapi-fetch/src/index.js index 261f5b147..f13ea6e7a 100644 --- a/packages/openapi-fetch/src/index.js +++ b/packages/openapi-fetch/src/index.js @@ -52,6 +52,7 @@ export default function createClient(clientOptions) { querySerializer: requestQuerySerializer, bodySerializer = globalBodySerializer ?? defaultBodySerializer, body, + middleware: fetchMiddlewares = [], ...init } = fetchOptions || {}; let finalBaseUrl = baseUrl; @@ -99,6 +100,12 @@ export default function createClient(clientOptions) { params.header, ); + const finalMiddlewares = [ + // Client level middleware take priority over request-level middleware + ...(Array.isArray(middlewares) && middlewares), + ...(Array.isArray(fetchMiddlewares) && fetchMiddlewares), + ]; + const requestInit = { redirect: "follow", ...baseOptions, @@ -122,7 +129,7 @@ export default function createClient(clientOptions) { } } - if (middlewares.length) { + if (finalMiddlewares.length) { id = randomID(); // middleware (request) @@ -133,7 +140,7 @@ export default function createClient(clientOptions) { querySerializer, bodySerializer, }); - for (const m of middlewares) { + for (const m of finalMiddlewares) { if (m && typeof m === "object" && typeof m.onRequest === "function") { const result = await m.onRequest({ request, @@ -164,9 +171,9 @@ export default function createClient(clientOptions) { let errorAfterMiddleware = error; // middleware (error) // execute in reverse-array order (first priority gets last transform) - if (middlewares.length) { - for (let i = middlewares.length - 1; i >= 0; i--) { - const m = middlewares[i]; + if (finalMiddlewares.length) { + for (let i = finalMiddlewares.length - 1; i >= 0; i--) { + const m = finalMiddlewares[i]; if (m && typeof m === "object" && typeof m.onError === "function") { const result = await m.onError({ request, @@ -203,9 +210,9 @@ export default function createClient(clientOptions) { // middleware (response) // execute in reverse-array order (first priority gets last transform) - if (middlewares.length) { - for (let i = middlewares.length - 1; i >= 0; i--) { - const m = middlewares[i]; + if (finalMiddlewares.length) { + for (let i = finalMiddlewares.length - 1; i >= 0; i--) { + const m = finalMiddlewares[i]; if (m && typeof m === "object" && typeof m.onResponse === "function") { const result = await m.onResponse({ request, @@ -663,3 +670,13 @@ export function removeTrailingSlash(url) { } return url; } + +/** + * Validate middleware object + * @type {import("./index.js").validateMiddleware} + */ +export function validateMiddleware(middleware) { + if (typeof middleware !== "object" || !("onRequest" in middleware || "onResponse" in v || "onError" in middleware)) { + throw new Error("Middleware must be an object with one of `onRequest()`, `onResponse() or `onError()`"); + } +} diff --git a/packages/openapi-fetch/test/middleware/middleware.test.ts b/packages/openapi-fetch/test/middleware/middleware.test.ts index f215e9cfe..b45843ea7 100644 --- a/packages/openapi-fetch/test/middleware/middleware.test.ts +++ b/packages/openapi-fetch/test/middleware/middleware.test.ts @@ -505,3 +505,118 @@ test("skips onResponse handlers when response is returned from onRequest", async expect(onResponseCalled).toBe(false); }); + +test('it should enable a middleware to be added via the "middleware" request option', async () => { + let actualRequest = new Request("https://nottherealurl.fake"); + const client = createObservedClient({}, async (req) => { + actualRequest = new Request(req); + return Response.json({}); + }); + + await client.GET("/posts/{id}", { + params: { path: { id: 123 } }, + middleware: [ + { + async onRequest({ request }) { + return new Request("https://foo.bar/api/v1", { + ...request, + method: "OPTIONS", + headers: { foo: "bar" }, + }); + }, + }, + ], + }); + + expect(actualRequest.url).toBe("https://foo.bar/api/v1"); + expect(actualRequest.method).toBe("OPTIONS"); + expect(actualRequest.headers.get("foo")).toBe("bar"); +}); + +test("add middleware at the request level", async () => { + let actualRequest = new Request("https://nottherealurl.fake"); + const client = createObservedClient({}, async (req) => { + actualRequest = new Request(req); + return Response.json({}); + }); + + await client.GET("/posts/{id}", { + params: { path: { id: 123 } }, + middleware: [ + { + async onRequest({ request }) { + return new Request("https://foo.bar/api/v1", { + ...request, + method: "OPTIONS", + headers: { foo: "bar" }, + }); + }, + }, + ], + }); + + expect(actualRequest.url).toBe("https://foo.bar/api/v1"); + expect(actualRequest.method).toBe("OPTIONS"); + expect(actualRequest.headers.get("foo")).toBe("bar"); +}); + +test("executes a middleware at the client and request request level in the correct orders", async () => { + let actualRequest = new Request("https://nottherealurl.fake"); + const client = createObservedClient({}, async (req) => { + actualRequest = new Request(req); + return Response.json({}); + }); + // this middleware passes along the “step” header + // for both requests and responses, but first checks if + // it received the end result of the previous middleware step + client.use( + { + async onRequest({ request }) { + request.headers.set("step", "A"); + return request; + }, + async onResponse({ response }) { + if (response.headers.get("step") === "B") { + const headers = new Headers(response.headers); + headers.set("step", "A"); + return new Response(response.body, { ...response, headers }); + } + }, + }, + { + async onRequest({ request }) { + request.headers.set("step", "B"); + return request; + }, + async onResponse({ response }) { + const headers = new Headers(response.headers); + headers.set("step", "B"); + if (response.headers.get("step") === "C") { + return new Response(response.body, { ...response, headers }); + } + }, + }, + ); + + const { response } = await client.GET("/posts/{id}", { + params: { path: { id: 123 } }, + middleware: [ + { + onRequest({ request }) { + request.headers.set("step", "C"); + return request; + }, + onResponse({ response }) { + response.headers.set("step", "C"); + return response; + }, + }, + ], + }); + + // assert requests ended up on step C (array order) + expect(actualRequest.headers.get("step")).toBe("C"); + + // assert responses ended up on step A (reverse order) + expect(response.headers.get("step")).toBe("A"); +}); From 7f5ad65fc60821a147ad5f0cb54f4a1bcef61f68 Mon Sep 17 00:00:00 2001 From: Carson Bruce Date: Fri, 3 Oct 2025 09:10:20 -0400 Subject: [PATCH 2/2] feat(openapi-fetch): Align tests and vars with feedback --- packages/openapi-fetch/src/index.js | 27 +--- .../test/middleware/middleware.test.ts | 132 ++++-------------- 2 files changed, 34 insertions(+), 125 deletions(-) diff --git a/packages/openapi-fetch/src/index.js b/packages/openapi-fetch/src/index.js index f13ea6e7a..be3b153a3 100644 --- a/packages/openapi-fetch/src/index.js +++ b/packages/openapi-fetch/src/index.js @@ -34,7 +34,7 @@ export default function createClient(clientOptions) { } = { ...clientOptions }; requestInitExt = supportsRequestInitExt() ? requestInitExt : undefined; baseUrl = removeTrailingSlash(baseUrl); - const middlewares = []; + const globalMiddlewares = []; /** * Per-request fetch (keeps settings created in createClient() @@ -52,7 +52,7 @@ export default function createClient(clientOptions) { querySerializer: requestQuerySerializer, bodySerializer = globalBodySerializer ?? defaultBodySerializer, body, - middleware: fetchMiddlewares = [], + middleware: requestMiddlewares = [], ...init } = fetchOptions || {}; let finalBaseUrl = baseUrl; @@ -100,11 +100,8 @@ export default function createClient(clientOptions) { params.header, ); - const finalMiddlewares = [ - // Client level middleware take priority over request-level middleware - ...(Array.isArray(middlewares) && middlewares), - ...(Array.isArray(fetchMiddlewares) && fetchMiddlewares), - ]; + // Client level middleware take priority over request-level middleware + const finalMiddlewares = [...globalMiddlewares, ...requestMiddlewares]; const requestInit = { redirect: "follow", @@ -302,15 +299,15 @@ export default function createClient(clientOptions) { if (typeof m !== "object" || !("onRequest" in m || "onResponse" in m || "onError" in m)) { throw new Error("Middleware must be an object with one of `onRequest()`, `onResponse() or `onError()`"); } - middlewares.push(m); + globalMiddlewares.push(m); } }, /** Unregister middleware */ eject(...middleware) { for (const m of middleware) { - const i = middlewares.indexOf(m); + const i = globalMiddlewares.indexOf(m); if (i !== -1) { - middlewares.splice(i, 1); + globalMiddlewares.splice(i, 1); } } }, @@ -670,13 +667,3 @@ export function removeTrailingSlash(url) { } return url; } - -/** - * Validate middleware object - * @type {import("./index.js").validateMiddleware} - */ -export function validateMiddleware(middleware) { - if (typeof middleware !== "object" || !("onRequest" in middleware || "onResponse" in v || "onError" in middleware)) { - throw new Error("Middleware must be an object with one of `onRequest()`, `onResponse() or `onError()`"); - } -} diff --git a/packages/openapi-fetch/test/middleware/middleware.test.ts b/packages/openapi-fetch/test/middleware/middleware.test.ts index b45843ea7..2d96ab457 100644 --- a/packages/openapi-fetch/test/middleware/middleware.test.ts +++ b/packages/openapi-fetch/test/middleware/middleware.test.ts @@ -194,16 +194,33 @@ test("executes in expected order", async () => { return request; }, onResponse({ response }) { - response.headers.set("step", "C"); - return response; + const headers = new Headers(response.headers); + headers.set("step", "C"); + if (response.headers.get("step") === "D") { + return new Response(response.body, { ...response, headers }); + } }, }, ); - const { response } = await client.GET("/posts/{id}", { params: { path: { id: 123 } } }); + const { response } = await client.GET("/posts/{id}", { + params: { path: { id: 123 } }, + middleware: [ + { + onRequest({ request }) { + request.headers.set("step", "D"); + return request; + }, + onResponse({ response }) { + response.headers.set("step", "D"); + return response; + }, + }, + ], + }); // assert requests ended up on step C (array order) - expect(actualRequest.headers.get("step")).toBe("C"); + expect(actualRequest.headers.get("step")).toBe("D"); // assert responses ended up on step A (reverse order) expect(response.headers.get("step")).toBe("A"); @@ -506,117 +523,22 @@ test("skips onResponse handlers when response is returned from onRequest", async expect(onResponseCalled).toBe(false); }); -test('it should enable a middleware to be added via the "middleware" request option', async () => { - let actualRequest = new Request("https://nottherealurl.fake"); - const client = createObservedClient({}, async (req) => { - actualRequest = new Request(req); - return Response.json({}); - }); - - await client.GET("/posts/{id}", { - params: { path: { id: 123 } }, - middleware: [ - { - async onRequest({ request }) { - return new Request("https://foo.bar/api/v1", { - ...request, - method: "OPTIONS", - headers: { foo: "bar" }, - }); - }, - }, - ], - }); - - expect(actualRequest.url).toBe("https://foo.bar/api/v1"); - expect(actualRequest.method).toBe("OPTIONS"); - expect(actualRequest.headers.get("foo")).toBe("bar"); -}); - test("add middleware at the request level", async () => { - let actualRequest = new Request("https://nottherealurl.fake"); - const client = createObservedClient({}, async (req) => { - actualRequest = new Request(req); - return Response.json({}); - }); - - await client.GET("/posts/{id}", { - params: { path: { id: 123 } }, - middleware: [ - { - async onRequest({ request }) { - return new Request("https://foo.bar/api/v1", { - ...request, - method: "OPTIONS", - headers: { foo: "bar" }, - }); - }, - }, - ], - }); - - expect(actualRequest.url).toBe("https://foo.bar/api/v1"); - expect(actualRequest.method).toBe("OPTIONS"); - expect(actualRequest.headers.get("foo")).toBe("bar"); -}); - -test("executes a middleware at the client and request request level in the correct orders", async () => { - let actualRequest = new Request("https://nottherealurl.fake"); - const client = createObservedClient({}, async (req) => { - actualRequest = new Request(req); - return Response.json({}); + const customResponse = Response.json({}); + const client = createObservedClient({}, async () => { + throw new Error("unexpected call to fetch"); }); - // this middleware passes along the “step” header - // for both requests and responses, but first checks if - // it received the end result of the previous middleware step - client.use( - { - async onRequest({ request }) { - request.headers.set("step", "A"); - return request; - }, - async onResponse({ response }) { - if (response.headers.get("step") === "B") { - const headers = new Headers(response.headers); - headers.set("step", "A"); - return new Response(response.body, { ...response, headers }); - } - }, - }, - { - async onRequest({ request }) { - request.headers.set("step", "B"); - return request; - }, - async onResponse({ response }) { - const headers = new Headers(response.headers); - headers.set("step", "B"); - if (response.headers.get("step") === "C") { - return new Response(response.body, { ...response, headers }); - } - }, - }, - ); const { response } = await client.GET("/posts/{id}", { params: { path: { id: 123 } }, middleware: [ { - onRequest({ request }) { - request.headers.set("step", "C"); - return request; - }, - onResponse({ response }) { - response.headers.set("step", "C"); - return response; + async onRequest() { + return customResponse; }, }, ], }); - // assert requests ended up on step C (array order) - expect(actualRequest.headers.get("step")).toBe("C"); - - // assert responses ended up on step A (reverse order) - expect(response.headers.get("step")).toBe("A"); + expect(response).toBe(customResponse); });