Skip to content

Commit b5a475e

Browse files
feat(openapi-fetch): add onError handler to middleware
1 parent 781cf92 commit b5a475e

File tree

3 files changed

+138
-6
lines changed

3 files changed

+138
-6
lines changed

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

+11-1
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import type {
66
MediaType,
77
OperationRequestBodyContent,
88
PathsWithMethod,
9-
ResponseObjectMap,
109
RequiredKeysOf,
10+
ResponseObjectMap,
1111
SuccessResponse,
1212
} from "openapi-typescript-helpers";
1313

@@ -152,15 +152,25 @@ type MiddlewareOnRequest = (
152152
type MiddlewareOnResponse = (
153153
options: MiddlewareCallbackParams & { response: Response },
154154
) => void | Response | undefined | Promise<Response | undefined | void>;
155+
type MiddlewareOnError = (
156+
options: MiddlewareCallbackParams & { error: unknown },
157+
) => void | Response | Error | Promise<void | Response | Error>;
155158

156159
export type Middleware =
157160
| {
158161
onRequest: MiddlewareOnRequest;
159162
onResponse?: MiddlewareOnResponse;
163+
onError?: MiddlewareOnError;
160164
}
161165
| {
162166
onRequest?: MiddlewareOnRequest;
163167
onResponse: MiddlewareOnResponse;
168+
onError?: MiddlewareOnError;
169+
}
170+
| {
171+
onRequest?: MiddlewareOnRequest;
172+
onResponse?: MiddlewareOnResponse;
173+
onError: MiddlewareOnError;
164174
};
165175

166176
/** This type helper makes the 2nd function param required if params/requestBody are required; otherwise, optional */

packages/openapi-fetch/src/index.js

+45-3
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,49 @@ export default function createClient(clientOptions) {
124124
}
125125

126126
// fetch!
127-
let response = await fetch(request);
127+
let response;
128+
try {
129+
response = await fetch(request);
130+
} catch (error) {
131+
let errorAfterMiddleware = error;
132+
// middleware (error)
133+
// execute in reverse-array order (first priority gets last transform)
134+
if (middlewares.length) {
135+
for (let i = middlewares.length - 1; i >= 0; i--) {
136+
const m = middlewares[i];
137+
if (m && typeof m === "object" && typeof m.onError === "function") {
138+
const result = await m.onError({
139+
request,
140+
error: errorAfterMiddleware,
141+
schemaPath,
142+
params,
143+
options,
144+
id,
145+
});
146+
if (result) {
147+
// if error is handled by returning a response, skip remaining middleware
148+
if (result instanceof Response) {
149+
errorAfterMiddleware = undefined;
150+
response = result;
151+
break;
152+
}
153+
154+
if (result instanceof Error) {
155+
errorAfterMiddleware = result;
156+
continue;
157+
}
158+
159+
throw new Error("onError: must return new Response() or instance of Error");
160+
}
161+
}
162+
}com
163+
}
164+
165+
// rethrow error if not handled by middleware
166+
if (errorAfterMiddleware) {
167+
throw errorAfterMiddleware;
168+
}
169+
}
128170

129171
// middleware (response)
130172
// execute in reverse-array order (first priority gets last transform)
@@ -213,8 +255,8 @@ export default function createClient(clientOptions) {
213255
if (!m) {
214256
continue;
215257
}
216-
if (typeof m !== "object" || !("onRequest" in m || "onResponse" in m)) {
217-
throw new Error("Middleware must be an object with one of `onRequest()` or `onResponse()`");
258+
if (typeof m !== "object" || !("onRequest" in m || "onResponse" in m || "onError" in m)) {
259+
throw new Error("Middleware must be an object with one of `onRequest()`, `onResponse() or `onError()`");
218260
}
219261
middlewares.push(m);
220262
}

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

+82-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { expect, test, expectTypeOf, assertType } from "vitest";
2-
import { createObservedClient } from "../helpers.js";
1+
import { assertType, expect, expectTypeOf, test } from "vitest";
32
import type { Middleware, MiddlewareCallbackParams } from "../../src/index.js";
3+
import { createObservedClient } from "../helpers.js";
44
import type { paths } from "./schemas/middleware.js";
55

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

99+
test("returns original errors if nothing is returned", async () => {
100+
const actualError = new Error();
101+
const client = createObservedClient<paths>({}, async (req) => {
102+
throw actualError;
103+
});
104+
client.use({
105+
onError({ error }) {
106+
expect(error).toBe(actualError);
107+
return;
108+
},
109+
});
110+
111+
try {
112+
await client.GET("/posts/{id}", { params: { path: { id: 123 } } });
113+
} catch (thrownError) {
114+
expect(thrownError).toBe(actualError);
115+
}
116+
});
117+
118+
test("can modify errors", async () => {
119+
const actualError = new Error();
120+
const modifiedError = new Error();
121+
const client = createObservedClient<paths>({}, async (req) => {
122+
throw actualError;
123+
});
124+
client.use({
125+
onError() {
126+
return modifiedError;
127+
},
128+
});
129+
130+
try {
131+
await client.GET("/posts/{id}", { params: { path: { id: 123 } } });
132+
} catch (thrownError) {
133+
expect(thrownError).toBe(modifiedError);
134+
}
135+
});
136+
137+
test("can catch errors and return a response instead", async () => {
138+
const actualError = new Error();
139+
const customResponse = Response.json({});
140+
const client = createObservedClient<paths>({}, async (req) => {
141+
throw actualError;
142+
});
143+
client.use({
144+
onError({ error }) {
145+
expect(error).toBe(actualError);
146+
return customResponse;
147+
},
148+
});
149+
150+
const { response } = await client.GET("/posts/{id}", { params: { path: { id: 123 } } });
151+
152+
expect(response).toBe(customResponse);
153+
});
154+
99155
test("executes in expected order", async () => {
100156
let actualRequest = new Request("https://nottherealurl.fake");
101157
const client = createObservedClient<paths>({}, async (req) => {
@@ -153,6 +209,30 @@ test("executes in expected order", async () => {
153209
expect(response.headers.get("step")).toBe("A");
154210
});
155211

212+
test("executes error handlers in expected order", async () => {
213+
const actualError = new Error();
214+
const modifiedError = new Error();
215+
const customResponse = Response.json({});
216+
const client = createObservedClient<paths>({}, async (req) => {
217+
throw actualError;
218+
});
219+
client.use({
220+
onError({ error }) {
221+
expect(error).toBe(modifiedError);
222+
return customResponse;
223+
},
224+
});
225+
client.use({
226+
onError() {
227+
return modifiedError;
228+
},
229+
});
230+
231+
const { response } = await client.GET("/posts/{id}", { params: { path: { id: 123 } } });
232+
233+
expect(response).toBe(customResponse);
234+
});
235+
156236
test("receives correct options", async () => {
157237
let requestBaseUrl = "";
158238

0 commit comments

Comments
 (0)