Skip to content

Commit bd35e6b

Browse files
author
Carson Bruce
committed
feat(openapi-fetch): Enable per request middleware
1 parent 7527d1e commit bd35e6b

File tree

4 files changed

+146
-8
lines changed

4 files changed

+146
-8
lines changed

.changeset/nice-parts-poke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-fetch": minor
3+
---
4+
5+
Enable request level middlewares option

packages/openapi-fetch/src/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export type RequestOptions<T> = ParamsOption<T> &
120120
parseAs?: ParseAs;
121121
fetch?: ClientOptions["fetch"];
122122
headers?: HeadersOptions;
123+
middleware?: Middleware[];
123124
};
124125

125126
export type MergedOptions<T = unknown> = {

packages/openapi-fetch/src/index.js

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export default function createClient(clientOptions) {
5252
querySerializer: requestQuerySerializer,
5353
bodySerializer = globalBodySerializer ?? defaultBodySerializer,
5454
body,
55+
middleware: fetchMiddlewares = [],
5556
...init
5657
} = fetchOptions || {};
5758
let finalBaseUrl = baseUrl;
@@ -99,6 +100,12 @@ export default function createClient(clientOptions) {
99100
params.header,
100101
);
101102

103+
const finalMiddlewares = [
104+
// Client level middleware take priority over request-level middleware
105+
...(Array.isArray(middlewares) && middlewares),
106+
...(Array.isArray(fetchMiddlewares) && fetchMiddlewares),
107+
];
108+
102109
const requestInit = {
103110
redirect: "follow",
104111
...baseOptions,
@@ -122,7 +129,7 @@ export default function createClient(clientOptions) {
122129
}
123130
}
124131

125-
if (middlewares.length) {
132+
if (finalMiddlewares.length) {
126133
id = randomID();
127134

128135
// middleware (request)
@@ -133,7 +140,7 @@ export default function createClient(clientOptions) {
133140
querySerializer,
134141
bodySerializer,
135142
});
136-
for (const m of middlewares) {
143+
for (const m of finalMiddlewares) {
137144
if (m && typeof m === "object" && typeof m.onRequest === "function") {
138145
const result = await m.onRequest({
139146
request,
@@ -164,9 +171,9 @@ export default function createClient(clientOptions) {
164171
let errorAfterMiddleware = error;
165172
// middleware (error)
166173
// execute in reverse-array order (first priority gets last transform)
167-
if (middlewares.length) {
168-
for (let i = middlewares.length - 1; i >= 0; i--) {
169-
const m = middlewares[i];
174+
if (finalMiddlewares.length) {
175+
for (let i = finalMiddlewares.length - 1; i >= 0; i--) {
176+
const m = finalMiddlewares[i];
170177
if (m && typeof m === "object" && typeof m.onError === "function") {
171178
const result = await m.onError({
172179
request,
@@ -203,9 +210,9 @@ export default function createClient(clientOptions) {
203210

204211
// middleware (response)
205212
// execute in reverse-array order (first priority gets last transform)
206-
if (middlewares.length) {
207-
for (let i = middlewares.length - 1; i >= 0; i--) {
208-
const m = middlewares[i];
213+
if (finalMiddlewares.length) {
214+
for (let i = finalMiddlewares.length - 1; i >= 0; i--) {
215+
const m = finalMiddlewares[i];
209216
if (m && typeof m === "object" && typeof m.onResponse === "function") {
210217
const result = await m.onResponse({
211218
request,
@@ -663,3 +670,13 @@ export function removeTrailingSlash(url) {
663670
}
664671
return url;
665672
}
673+
674+
/**
675+
* Validate middleware object
676+
* @type {import("./index.js").validateMiddleware}
677+
*/
678+
export function validateMiddleware(middleware) {
679+
if (typeof middleware !== "object" || !("onRequest" in middleware || "onResponse" in v || "onError" in middleware)) {
680+
throw new Error("Middleware must be an object with one of `onRequest()`, `onResponse() or `onError()`");
681+
}
682+
}

packages/openapi-fetch/test/middleware/middleware.test.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,3 +505,118 @@ test("skips onResponse handlers when response is returned from onRequest", async
505505

506506
expect(onResponseCalled).toBe(false);
507507
});
508+
509+
test('it should enable a middleware to be added via the "middleware" request option', async () => {
510+
let actualRequest = new Request("https://nottherealurl.fake");
511+
const client = createObservedClient<paths>({}, async (req) => {
512+
actualRequest = new Request(req);
513+
return Response.json({});
514+
});
515+
516+
await client.GET("/posts/{id}", {
517+
params: { path: { id: 123 } },
518+
middleware: [
519+
{
520+
async onRequest({ request }) {
521+
return new Request("https://foo.bar/api/v1", {
522+
...request,
523+
method: "OPTIONS",
524+
headers: { foo: "bar" },
525+
});
526+
},
527+
},
528+
],
529+
});
530+
531+
expect(actualRequest.url).toBe("https://foo.bar/api/v1");
532+
expect(actualRequest.method).toBe("OPTIONS");
533+
expect(actualRequest.headers.get("foo")).toBe("bar");
534+
});
535+
536+
test("add middleware at the request level", async () => {
537+
let actualRequest = new Request("https://nottherealurl.fake");
538+
const client = createObservedClient<paths>({}, async (req) => {
539+
actualRequest = new Request(req);
540+
return Response.json({});
541+
});
542+
543+
await client.GET("/posts/{id}", {
544+
params: { path: { id: 123 } },
545+
middleware: [
546+
{
547+
async onRequest({ request }) {
548+
return new Request("https://foo.bar/api/v1", {
549+
...request,
550+
method: "OPTIONS",
551+
headers: { foo: "bar" },
552+
});
553+
},
554+
},
555+
],
556+
});
557+
558+
expect(actualRequest.url).toBe("https://foo.bar/api/v1");
559+
expect(actualRequest.method).toBe("OPTIONS");
560+
expect(actualRequest.headers.get("foo")).toBe("bar");
561+
});
562+
563+
test("executes a middleware at the client and request request level in the correct orders", async () => {
564+
let actualRequest = new Request("https://nottherealurl.fake");
565+
const client = createObservedClient<paths>({}, async (req) => {
566+
actualRequest = new Request(req);
567+
return Response.json({});
568+
});
569+
// this middleware passes along the “step” header
570+
// for both requests and responses, but first checks if
571+
// it received the end result of the previous middleware step
572+
client.use(
573+
{
574+
async onRequest({ request }) {
575+
request.headers.set("step", "A");
576+
return request;
577+
},
578+
async onResponse({ response }) {
579+
if (response.headers.get("step") === "B") {
580+
const headers = new Headers(response.headers);
581+
headers.set("step", "A");
582+
return new Response(response.body, { ...response, headers });
583+
}
584+
},
585+
},
586+
{
587+
async onRequest({ request }) {
588+
request.headers.set("step", "B");
589+
return request;
590+
},
591+
async onResponse({ response }) {
592+
const headers = new Headers(response.headers);
593+
headers.set("step", "B");
594+
if (response.headers.get("step") === "C") {
595+
return new Response(response.body, { ...response, headers });
596+
}
597+
},
598+
},
599+
);
600+
601+
const { response } = await client.GET("/posts/{id}", {
602+
params: { path: { id: 123 } },
603+
middleware: [
604+
{
605+
onRequest({ request }) {
606+
request.headers.set("step", "C");
607+
return request;
608+
},
609+
onResponse({ response }) {
610+
response.headers.set("step", "C");
611+
return response;
612+
},
613+
},
614+
],
615+
});
616+
617+
// assert requests ended up on step C (array order)
618+
expect(actualRequest.headers.get("step")).toBe("C");
619+
620+
// assert responses ended up on step A (reverse order)
621+
expect(response.headers.get("step")).toBe("A");
622+
});

0 commit comments

Comments
 (0)