Skip to content

Commit ebe56f3

Browse files
authored
feat(openapi-fetch): Allow returning Response from onRequest callback (#2091)
* feat(openapi-fetch): Allow returning Response from onRequest callback * feat(openapi-fetch): Allow returning Response from onRequest callback
1 parent 82e98b4 commit ebe56f3

File tree

6 files changed

+161
-56
lines changed

6 files changed

+161
-56
lines changed

.changeset/orange-rules-sneeze.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-fetch": patch
3+
---
4+
5+
Allow returning Response from onRequest callback

docs/openapi-fetch/api.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ And the `onError` callback receives an additional `error` property:
268268

269269
Each middleware callback can return:
270270

271-
- **onRequest**: Either a `Request` to modify the request, or `undefined` to leave it untouched (skip)
271+
- **onRequest**: A `Request` to modify the request, a `Response` to short-circuit the middleware chain, or `undefined` to leave request untouched (skip)
272272
- **onResponse**: Either a `Response` to modify the response, or `undefined` to leave it untouched (skip)
273273
- **onError**: Either an `Error` to modify the error that is thrown, a `Response` which means that the `fetch` call will proceed as successful, or `undefined` to leave the error untouched (skip)
274274

docs/openapi-fetch/middleware-auth.md

+32
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,38 @@ onRequest({ schemaPath }) {
6464

6565
This will leave the request/response unmodified, and pass things off to the next middleware handler (if any). There’s no internal callback or observer library needed.
6666

67+
### Early Response
68+
69+
You can return a `Response` directly from `onRequest`, which will skip the actual request and remaining middleware chain. This is useful for cases such as deduplicating or caching responses to avoid unnecessary network requests.
70+
71+
```ts
72+
const cache = new Map<string, Response>();
73+
const getCacheKey = (request: Request) => `${request.method}:${request.url}`;
74+
75+
const cacheMiddleware: Middleware = {
76+
onRequest({ request }) {
77+
const key = getCacheKey(request);
78+
const cached = cache.get(key);
79+
if (cached) {
80+
// Return cached response, skipping actual request and remaining middleware chain
81+
return cached.clone();
82+
}
83+
},
84+
onResponse({ request, response }) {
85+
if (response.ok) {
86+
const key = getCacheKey(request);
87+
cache.set(key, response.clone());
88+
}
89+
}
90+
};
91+
```
92+
93+
When a middleware returns a `Response`:
94+
95+
* The request is not sent to the server
96+
* Subsequent `onRequest` handlers are skipped
97+
* `onResponse` handlers are skipped
98+
6799
### Throwing
68100

69101
Middleware can also be used to throw an error that `fetch()` wouldn’t normally, useful in libraries like [TanStack Query](https://tanstack.com/query/latest):

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ export interface MiddlewareCallbackParams {
150150

151151
type MiddlewareOnRequest = (
152152
options: MiddlewareCallbackParams,
153-
) => void | Request | undefined | Promise<Request | undefined | void>;
153+
) => void | Request | Response | undefined | Promise<Request | Response | undefined | void>;
154154
type MiddlewareOnResponse = (
155155
options: MiddlewareCallbackParams & { response: Response },
156156
) => void | Response | undefined | Promise<Response | undefined | void>;

packages/openapi-fetch/src/index.js

+60-54
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export default function createClient(clientOptions) {
9595
let id;
9696
let options;
9797
let request = new CustomRequest(createFinalURL(schemaPath, { baseUrl, params, querySerializer }), requestInit);
98+
let response;
9899

99100
/** Add custom parameters to Request object */
100101
for (const key in init) {
@@ -124,79 +125,84 @@ export default function createClient(clientOptions) {
124125
id,
125126
});
126127
if (result) {
127-
if (!(result instanceof CustomRequest)) {
128-
throw new Error("onRequest: must return new Request() when modifying the request");
128+
if (result instanceof CustomRequest) {
129+
request = result;
130+
} else if (result instanceof Response) {
131+
response = result;
132+
break;
133+
} else {
134+
throw new Error("onRequest: must return new Request() or Response() when modifying the request");
129135
}
130-
request = result;
131136
}
132137
}
133138
}
134139
}
135140

136-
// fetch!
137-
let response;
138-
try {
139-
response = await fetch(request, requestInitExt);
140-
} catch (error) {
141-
let errorAfterMiddleware = error;
142-
// middleware (error)
141+
if (!response) {
142+
// fetch!
143+
try {
144+
response = await fetch(request, requestInitExt);
145+
} catch (error) {
146+
let errorAfterMiddleware = error;
147+
// middleware (error)
148+
// execute in reverse-array order (first priority gets last transform)
149+
if (middlewares.length) {
150+
for (let i = middlewares.length - 1; i >= 0; i--) {
151+
const m = middlewares[i];
152+
if (m && typeof m === "object" && typeof m.onError === "function") {
153+
const result = await m.onError({
154+
request,
155+
error: errorAfterMiddleware,
156+
schemaPath,
157+
params,
158+
options,
159+
id,
160+
});
161+
if (result) {
162+
// if error is handled by returning a response, skip remaining middleware
163+
if (result instanceof Response) {
164+
errorAfterMiddleware = undefined;
165+
response = result;
166+
break;
167+
}
168+
169+
if (result instanceof Error) {
170+
errorAfterMiddleware = result;
171+
continue;
172+
}
173+
174+
throw new Error("onError: must return new Response() or instance of Error");
175+
}
176+
}
177+
}
178+
}
179+
180+
// rethrow error if not handled by middleware
181+
if (errorAfterMiddleware) {
182+
throw errorAfterMiddleware;
183+
}
184+
}
185+
186+
// middleware (response)
143187
// execute in reverse-array order (first priority gets last transform)
144188
if (middlewares.length) {
145189
for (let i = middlewares.length - 1; i >= 0; i--) {
146190
const m = middlewares[i];
147-
if (m && typeof m === "object" && typeof m.onError === "function") {
148-
const result = await m.onError({
191+
if (m && typeof m === "object" && typeof m.onResponse === "function") {
192+
const result = await m.onResponse({
149193
request,
150-
error: errorAfterMiddleware,
194+
response,
151195
schemaPath,
152196
params,
153197
options,
154198
id,
155199
});
156200
if (result) {
157-
// if error is handled by returning a response, skip remaining middleware
158-
if (result instanceof Response) {
159-
errorAfterMiddleware = undefined;
160-
response = result;
161-
break;
201+
if (!(result instanceof Response)) {
202+
throw new Error("onResponse: must return new Response() when modifying the response");
162203
}
163-
164-
if (result instanceof Error) {
165-
errorAfterMiddleware = result;
166-
continue;
167-
}
168-
169-
throw new Error("onError: must return new Response() or instance of Error");
170-
}
171-
}
172-
}
173-
}
174-
175-
// rethrow error if not handled by middleware
176-
if (errorAfterMiddleware) {
177-
throw errorAfterMiddleware;
178-
}
179-
}
180-
181-
// middleware (response)
182-
// execute in reverse-array order (first priority gets last transform)
183-
if (middlewares.length) {
184-
for (let i = middlewares.length - 1; i >= 0; i--) {
185-
const m = middlewares[i];
186-
if (m && typeof m === "object" && typeof m.onResponse === "function") {
187-
const result = await m.onResponse({
188-
request,
189-
response,
190-
schemaPath,
191-
params,
192-
options,
193-
id,
194-
});
195-
if (result) {
196-
if (!(result instanceof Response)) {
197-
throw new Error("onResponse: must return new Response() when modifying the response");
204+
response = result;
198205
}
199-
response = result;
200206
}
201207
}
202208
}

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

+62
Original file line numberDiff line numberDiff line change
@@ -443,3 +443,65 @@ test("type error occurs only when neither onRequest nor onResponse is specified"
443443
assertType<Middleware>({ onResponse });
444444
assertType<Middleware>({ onRequest, onResponse });
445445
});
446+
447+
test("can return response directly from onRequest", async () => {
448+
const customResponse = Response.json({});
449+
450+
const client = createObservedClient<paths>({}, () => {
451+
throw new Error("unexpected call to fetch");
452+
});
453+
454+
client.use({
455+
async onRequest() {
456+
return customResponse;
457+
},
458+
});
459+
460+
const { response } = await client.GET("/posts/{id}", {
461+
params: { path: { id: 123 } },
462+
});
463+
464+
expect(response).toBe(customResponse);
465+
});
466+
467+
test("skips subsequent onRequest handlers when response is returned", async () => {
468+
let onRequestCalled = false;
469+
const client = createObservedClient<paths>();
470+
471+
client.use(
472+
{
473+
async onRequest() {
474+
return Response.json({});
475+
},
476+
},
477+
{
478+
async onRequest() {
479+
onRequestCalled = true;
480+
return undefined;
481+
},
482+
},
483+
);
484+
485+
await client.GET("/posts/{id}", { params: { path: { id: 123 } } });
486+
487+
expect(onRequestCalled).toBe(false);
488+
});
489+
490+
test("skips onResponse handlers when response is returned from onRequest", async () => {
491+
let onResponseCalled = false;
492+
const client = createObservedClient<paths>();
493+
494+
client.use({
495+
async onRequest() {
496+
return Response.json({});
497+
},
498+
async onResponse() {
499+
onResponseCalled = true;
500+
return undefined;
501+
},
502+
});
503+
504+
await client.GET("/posts/{id}", { params: { path: { id: 123 } } });
505+
506+
expect(onResponseCalled).toBe(false);
507+
});

0 commit comments

Comments
 (0)