Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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/nice-parts-poke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-fetch": minor
---

Enable request level middlewares option
1 change: 1 addition & 0 deletions packages/openapi-fetch/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export type RequestOptions<T> = ParamsOption<T> &
parseAs?: ParseAs;
fetch?: ClientOptions["fetch"];
headers?: HeadersOptions;
middleware?: Middleware[];
};

export type MergedOptions<T = unknown> = {
Expand Down
33 changes: 25 additions & 8 deletions packages/openapi-fetch/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export default function createClient(clientOptions) {
querySerializer: requestQuerySerializer,
bodySerializer = globalBodySerializer ?? defaultBodySerializer,
body,
middleware: fetchMiddlewares = [],
...init
} = fetchOptions || {};
let finalBaseUrl = baseUrl;
Expand Down Expand Up @@ -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,
Expand All @@ -122,7 +129,7 @@ export default function createClient(clientOptions) {
}
}

if (middlewares.length) {
if (finalMiddlewares.length) {
id = randomID();

// middleware (request)
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()`");
}
}
115 changes: 115 additions & 0 deletions packages/openapi-fetch/test/middleware/middleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<paths>({}, 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<paths>({}, 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<paths>({}, 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");
});
Loading