From 61e7b768bce47a66bee6f419b02cec931ce943b9 Mon Sep 17 00:00:00 2001 From: Marvin Luchs Date: Sun, 27 Oct 2024 13:11:46 +0100 Subject: [PATCH] feat(openapi-fetch): add onError handler to middleware --- packages/openapi-fetch/src/index.d.ts | 12 ++- packages/openapi-fetch/src/index.js | 48 ++++++++++- .../test/middleware/middleware.test.ts | 84 ++++++++++++++++++- 3 files changed, 138 insertions(+), 6 deletions(-) diff --git a/packages/openapi-fetch/src/index.d.ts b/packages/openapi-fetch/src/index.d.ts index a7b26a1be..6686b302d 100644 --- a/packages/openapi-fetch/src/index.d.ts +++ b/packages/openapi-fetch/src/index.d.ts @@ -6,8 +6,8 @@ import type { MediaType, OperationRequestBodyContent, PathsWithMethod, - ResponseObjectMap, RequiredKeysOf, + ResponseObjectMap, SuccessResponse, } from "openapi-typescript-helpers"; @@ -152,15 +152,25 @@ type MiddlewareOnRequest = ( type MiddlewareOnResponse = ( options: MiddlewareCallbackParams & { response: Response }, ) => void | Response | undefined | Promise; +type MiddlewareOnError = ( + options: MiddlewareCallbackParams & { error: unknown }, +) => void | Response | Error | Promise; 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 */ diff --git a/packages/openapi-fetch/src/index.js b/packages/openapi-fetch/src/index.js index 1b8502c6f..af3bd4759 100644 --- a/packages/openapi-fetch/src/index.js +++ b/packages/openapi-fetch/src/index.js @@ -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) @@ -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); } diff --git a/packages/openapi-fetch/test/middleware/middleware.test.ts b/packages/openapi-fetch/test/middleware/middleware.test.ts index a13ee8c57..9ccc23af4 100644 --- a/packages/openapi-fetch/test/middleware/middleware.test.ts +++ b/packages/openapi-fetch/test/middleware/middleware.test.ts @@ -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 () => { @@ -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({}, 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({}, 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({}, 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({}, async (req) => { @@ -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({}, 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 = "";