Skip to content

Commit

Permalink
feat(openapi-fetch): add onError handler to middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
luchsamapparat committed Oct 27, 2024
1 parent 781cf92 commit 61e7b76
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 6 deletions.
12 changes: 11 additions & 1 deletion packages/openapi-fetch/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import type {
MediaType,
OperationRequestBodyContent,
PathsWithMethod,
ResponseObjectMap,
RequiredKeysOf,
ResponseObjectMap,
SuccessResponse,
} from "openapi-typescript-helpers";

Expand Down Expand Up @@ -152,15 +152,25 @@ type MiddlewareOnRequest = (
type MiddlewareOnResponse = (
options: MiddlewareCallbackParams & { response: Response },
) => void | Response | undefined | Promise<Response | undefined | void>;
type MiddlewareOnError = (
options: MiddlewareCallbackParams & { error: unknown },
) => void | Response | Error | Promise<void | Response | Error>;

export type Middleware =
| {
onRequest: MiddlewareOnRequest;
onResponse?: MiddlewareOnResponse;
onError?: MiddlewareOnError;
}
| {
onRequest?: MiddlewareOnRequest;
onResponse: MiddlewareOnResponse;
onError?: MiddlewareOnError;
}
| {
onRequest?: MiddlewareOnRequest;
onResponse?: MiddlewareOnResponse;
onError: MiddlewareOnError;
};

/** This type helper makes the 2nd function param required if params/requestBody are required; otherwise, optional */
Expand Down
48 changes: 45 additions & 3 deletions packages/openapi-fetch/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,49 @@ export default function createClient(clientOptions) {
}

// fetch!
let response = await fetch(request);
let response;
try {
response = await fetch(request);
} catch (error) {
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 (m && typeof m === "object" && typeof m.onError === "function") {
const result = await m.onError({
request,
error: errorAfterMiddleware,
schemaPath,
params,
options,
id,
});
if (result) {
// if error is handled by returning a response, skip remaining middleware
if (result instanceof Response) {
errorAfterMiddleware = undefined;
response = result;
break;
}

if (result instanceof Error) {
errorAfterMiddleware = result;
continue;
}

throw new Error("onError: must return new Response() or instance of Error");
}
}
}
}

// rethrow error if not handled by middleware
if (errorAfterMiddleware) {
throw errorAfterMiddleware;
}
}

// middleware (response)
// execute in reverse-array order (first priority gets last transform)
Expand Down Expand Up @@ -213,8 +255,8 @@ export default function createClient(clientOptions) {
if (!m) {
continue;
}
if (typeof m !== "object" || !("onRequest" in m || "onResponse" in m)) {
throw new Error("Middleware must be an object with one of `onRequest()` or `onResponse()`");
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);
}
Expand Down
84 changes: 82 additions & 2 deletions packages/openapi-fetch/test/middleware/middleware.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect, test, expectTypeOf, assertType } from "vitest";
import { createObservedClient } from "../helpers.js";
import { assertType, expect, expectTypeOf, test } from "vitest";
import type { Middleware, MiddlewareCallbackParams } from "../../src/index.js";
import { createObservedClient } from "../helpers.js";
import type { paths } from "./schemas/middleware.js";

test("receives a UUID per-request", async () => {
Expand Down Expand Up @@ -96,6 +96,62 @@ test("can modify response", async () => {
expect(response.headers.get("middleware")).toBe("value");
});

test("returns original errors if nothing is returned", async () => {
const actualError = new Error();
const client = createObservedClient<paths>({}, async (req) => {
throw actualError;
});
client.use({
onError({ error }) {
expect(error).toBe(actualError);
return;
},
});

try {
await client.GET("/posts/{id}", { params: { path: { id: 123 } } });
} catch (thrownError) {
expect(thrownError).toBe(actualError);
}
});

test("can modify errors", async () => {
const actualError = new Error();
const modifiedError = new Error();
const client = createObservedClient<paths>({}, async (req) => {
throw actualError;
});
client.use({
onError() {
return modifiedError;
},
});

try {
await client.GET("/posts/{id}", { params: { path: { id: 123 } } });
} catch (thrownError) {
expect(thrownError).toBe(modifiedError);
}
});

test("can catch errors and return a response instead", async () => {
const actualError = new Error();
const customResponse = Response.json({});
const client = createObservedClient<paths>({}, async (req) => {
throw actualError;
});
client.use({
onError({ error }) {
expect(error).toBe(actualError);
return customResponse;
},
});

const { response } = await client.GET("/posts/{id}", { params: { path: { id: 123 } } });

expect(response).toBe(customResponse);
});

test("executes in expected order", async () => {
let actualRequest = new Request("https://nottherealurl.fake");
const client = createObservedClient<paths>({}, async (req) => {
Expand Down Expand Up @@ -153,6 +209,30 @@ test("executes in expected order", async () => {
expect(response.headers.get("step")).toBe("A");
});

test("executes error handlers in expected order", async () => {
const actualError = new Error();
const modifiedError = new Error();
const customResponse = Response.json({});
const client = createObservedClient<paths>({}, async (req) => {
throw actualError;
});
client.use({
onError({ error }) {
expect(error).toBe(modifiedError);
return customResponse;
},
});
client.use({
onError() {
return modifiedError;
},
});

const { response } = await client.GET("/posts/{id}", { params: { path: { id: 123 } } });

expect(response).toBe(customResponse);
});

test("receives correct options", async () => {
let requestBaseUrl = "";

Expand Down

0 comments on commit 61e7b76

Please sign in to comment.