Skip to content

Commit 3da8b4a

Browse files
authored
Merge branch 'main' into fix-request-safe-baseurl-override-default
2 parents 7052b84 + ebe56f3 commit 3da8b4a

File tree

6 files changed

+162
-60
lines changed

6 files changed

+162
-60
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

+61-58
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,8 @@ export default function createClient(clientOptions) {
9595

9696
let id;
9797
let options;
98-
let request = new CustomRequest(
99-
createFinalURL(schemaPath, { baseUrl: finalBaseUrl, params, querySerializer }),
100-
requestInit,
101-
);
98+
let request = new CustomRequest(createFinalURL(schemaPath, { baseUrl: finalBaseUrl, params, querySerializer }), requestInit);
99+
let response;
102100

103101
/** Add custom parameters to Request object */
104102
for (const key in init) {
@@ -128,79 +126,84 @@ export default function createClient(clientOptions) {
128126
id,
129127
});
130128
if (result) {
131-
if (!(result instanceof CustomRequest)) {
132-
throw new Error("onRequest: must return new Request() when modifying the request");
129+
if (result instanceof CustomRequest) {
130+
request = result;
131+
} else if (result instanceof Response) {
132+
response = result;
133+
break;
134+
} else {
135+
throw new Error("onRequest: must return new Request() or Response() when modifying the request");
133136
}
134-
request = result;
135137
}
136138
}
137139
}
138140
}
139141

140-
// fetch!
141-
let response;
142-
try {
143-
response = await fetch(request, requestInitExt);
144-
} catch (error) {
145-
let errorAfterMiddleware = error;
146-
// middleware (error)
142+
if (!response) {
143+
// fetch!
144+
try {
145+
response = await fetch(request, requestInitExt);
146+
} catch (error) {
147+
let errorAfterMiddleware = error;
148+
// middleware (error)
149+
// execute in reverse-array order (first priority gets last transform)
150+
if (middlewares.length) {
151+
for (let i = middlewares.length - 1; i >= 0; i--) {
152+
const m = middlewares[i];
153+
if (m && typeof m === "object" && typeof m.onError === "function") {
154+
const result = await m.onError({
155+
request,
156+
error: errorAfterMiddleware,
157+
schemaPath,
158+
params,
159+
options,
160+
id,
161+
});
162+
if (result) {
163+
// if error is handled by returning a response, skip remaining middleware
164+
if (result instanceof Response) {
165+
errorAfterMiddleware = undefined;
166+
response = result;
167+
break;
168+
}
169+
170+
if (result instanceof Error) {
171+
errorAfterMiddleware = result;
172+
continue;
173+
}
174+
175+
throw new Error("onError: must return new Response() or instance of Error");
176+
}
177+
}
178+
}
179+
}
180+
181+
// rethrow error if not handled by middleware
182+
if (errorAfterMiddleware) {
183+
throw errorAfterMiddleware;
184+
}
185+
}
186+
187+
// middleware (response)
147188
// execute in reverse-array order (first priority gets last transform)
148189
if (middlewares.length) {
149190
for (let i = middlewares.length - 1; i >= 0; i--) {
150191
const m = middlewares[i];
151-
if (m && typeof m === "object" && typeof m.onError === "function") {
152-
const result = await m.onError({
192+
if (m && typeof m === "object" && typeof m.onResponse === "function") {
193+
const result = await m.onResponse({
153194
request,
154-
error: errorAfterMiddleware,
195+
response,
155196
schemaPath,
156197
params,
157198
options,
158199
id,
159200
});
160201
if (result) {
161-
// if error is handled by returning a response, skip remaining middleware
162-
if (result instanceof Response) {
163-
errorAfterMiddleware = undefined;
164-
response = result;
165-
break;
202+
if (!(result instanceof Response)) {
203+
throw new Error("onResponse: must return new Response() when modifying the response");
166204
}
167-
168-
if (result instanceof Error) {
169-
errorAfterMiddleware = result;
170-
continue;
171-
}
172-
173-
throw new Error("onError: must return new Response() or instance of Error");
174-
}
175-
}
176-
}
177-
}
178-
179-
// rethrow error if not handled by middleware
180-
if (errorAfterMiddleware) {
181-
throw errorAfterMiddleware;
182-
}
183-
}
184-
185-
// middleware (response)
186-
// execute in reverse-array order (first priority gets last transform)
187-
if (middlewares.length) {
188-
for (let i = middlewares.length - 1; i >= 0; i--) {
189-
const m = middlewares[i];
190-
if (m && typeof m === "object" && typeof m.onResponse === "function") {
191-
const result = await m.onResponse({
192-
request,
193-
response,
194-
schemaPath,
195-
params,
196-
options,
197-
id,
198-
});
199-
if (result) {
200-
if (!(result instanceof Response)) {
201-
throw new Error("onResponse: must return new Response() when modifying the response");
205+
response = result;
202206
}
203-
response = result;
204207
}
205208
}
206209
}

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)