From fad2a509a7fbd19a032ed9976925cb8d1b5b6d94 Mon Sep 17 00:00:00 2001
From: Pavel Dubovitsky
Date: Sat, 4 Jan 2025 14:58:44 +0100
Subject: [PATCH 1/2] feat(openapi-fetch): Allow returning Response from
onRequest callback
---
.changeset/orange-rules-sneeze.md | 5 +
docs/openapi-fetch/api.md | 2 +-
docs/openapi-fetch/middleware-auth.md | 32 +++++
packages/openapi-fetch/src/index.d.ts | 2 +-
packages/openapi-fetch/src/index.js | 114 +++++++++---------
.../test/middleware/middleware.test.ts | 61 ++++++++++
6 files changed, 160 insertions(+), 56 deletions(-)
create mode 100644 .changeset/orange-rules-sneeze.md
diff --git a/.changeset/orange-rules-sneeze.md b/.changeset/orange-rules-sneeze.md
new file mode 100644
index 000000000..ce46d9c05
--- /dev/null
+++ b/.changeset/orange-rules-sneeze.md
@@ -0,0 +1,5 @@
+---
+"openapi-fetch": patch
+---
+
+Allow returning Response from onRequest callback
diff --git a/docs/openapi-fetch/api.md b/docs/openapi-fetch/api.md
index f5ea0c8c2..1a504c5d1 100644
--- a/docs/openapi-fetch/api.md
+++ b/docs/openapi-fetch/api.md
@@ -268,7 +268,7 @@ And the `onError` callback receives an additional `error` property:
Each middleware callback can return:
-- **onRequest**: Either a `Request` to modify the request, or `undefined` to leave it untouched (skip)
+- **onRequest**: A `Request` to modify the request, a `Response` to short-circuit the middleware chain, or `undefined` to leave request untouched (skip)
- **onResponse**: Either a `Response` to modify the response, or `undefined` to leave it untouched (skip)
- **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)
diff --git a/docs/openapi-fetch/middleware-auth.md b/docs/openapi-fetch/middleware-auth.md
index 3bd287a3a..12c0746c4 100644
--- a/docs/openapi-fetch/middleware-auth.md
+++ b/docs/openapi-fetch/middleware-auth.md
@@ -64,6 +64,38 @@ onRequest({ schemaPath }) {
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.
+### Early Response
+
+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.
+
+```ts
+const cache = new Map();
+const getCacheKey = (request: Request) => `${request.method}:${request.url}`;
+
+const cacheMiddleware: Middleware = {
+ onRequest({ request }) {
+ const key = getCacheKey(request);
+ const cached = cache.get(key);
+ if (cached) {
+ // Return cached response, skipping actual request and remaining middleware chain
+ return cached.clone();
+ }
+ },
+ onResponse({ request, response }) {
+ if (response.ok) {
+ const key = getCacheKey(request);
+ cache.set(key, response);
+ }
+ }
+};
+```
+
+When a middleware returns a `Response`:
+
+* The request is not sent to the server
+* Subsequent `onRequest` handlers are skipped
+* `onResponse` handlers are skipped
+
### Throwing
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):
diff --git a/packages/openapi-fetch/src/index.d.ts b/packages/openapi-fetch/src/index.d.ts
index 06d01d400..79dec3d77 100644
--- a/packages/openapi-fetch/src/index.d.ts
+++ b/packages/openapi-fetch/src/index.d.ts
@@ -150,7 +150,7 @@ export interface MiddlewareCallbackParams {
type MiddlewareOnRequest = (
options: MiddlewareCallbackParams,
-) => void | Request | undefined | Promise;
+) => void | Request | Response | undefined | Promise;
type MiddlewareOnResponse = (
options: MiddlewareCallbackParams & { response: Response },
) => void | Response | undefined | Promise;
diff --git a/packages/openapi-fetch/src/index.js b/packages/openapi-fetch/src/index.js
index 5e0c2fcd4..432b1ff33 100644
--- a/packages/openapi-fetch/src/index.js
+++ b/packages/openapi-fetch/src/index.js
@@ -95,6 +95,7 @@ export default function createClient(clientOptions) {
let id;
let options;
let request = new CustomRequest(createFinalURL(schemaPath, { baseUrl, params, querySerializer }), requestInit);
+ let response;
/** Add custom parameters to Request object */
for (const key in init) {
@@ -124,79 +125,84 @@ export default function createClient(clientOptions) {
id,
});
if (result) {
- if (!(result instanceof CustomRequest)) {
- throw new Error("onRequest: must return new Request() when modifying the request");
+ if (result instanceof CustomRequest) {
+ request = result;
+ } else if (result instanceof Response) {
+ response = result;
+ break;
+ } else {
+ throw new Error("onRequest: must return new Request() or Response() when modifying the request");
}
- request = result;
}
}
}
}
- // fetch!
- let response;
- try {
- response = await fetch(request, requestInitExt);
- } catch (error) {
- let errorAfterMiddleware = error;
- // middleware (error)
+ if (!response) {
+ // fetch!
+ try {
+ response = await fetch(request, requestInitExt);
+ } 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)
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({
+ if (m && typeof m === "object" && typeof m.onResponse === "function") {
+ const result = await m.onResponse({
request,
- error: errorAfterMiddleware,
+ response,
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 Response)) {
+ throw new Error("onResponse: must return new Response() when modifying the response");
}
-
- 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)
- if (middlewares.length) {
- for (let i = middlewares.length - 1; i >= 0; i--) {
- const m = middlewares[i];
- if (m && typeof m === "object" && typeof m.onResponse === "function") {
- const result = await m.onResponse({
- request,
- response,
- schemaPath,
- params,
- options,
- id,
- });
- if (result) {
- if (!(result instanceof Response)) {
- throw new Error("onResponse: must return new Response() when modifying the response");
+ response = result;
}
- response = result;
}
}
}
diff --git a/packages/openapi-fetch/test/middleware/middleware.test.ts b/packages/openapi-fetch/test/middleware/middleware.test.ts
index 9ccc23af4..d1643fbd3 100644
--- a/packages/openapi-fetch/test/middleware/middleware.test.ts
+++ b/packages/openapi-fetch/test/middleware/middleware.test.ts
@@ -443,3 +443,64 @@ test("type error occurs only when neither onRequest nor onResponse is specified"
assertType({ onResponse });
assertType({ onRequest, onResponse });
});
+
+test("can return response directly from onRequest", async () => {
+ const customResponse = Response.json({});
+ const client = createObservedClient();
+
+ client.use({
+ async onRequest() {
+ return customResponse;
+ },
+ });
+
+ const { response } = await client.GET("/posts/{id}", {
+ params: { path: { id: 123 } },
+ });
+
+ expect(response).toBe(customResponse);
+});
+
+test("skips subsequent onRequest handlers when response is returned", async () => {
+ let onRequestCalled = false;
+ const customResponse = Response.json({});
+ const client = createObservedClient();
+
+ client.use(
+ {
+ async onRequest() {
+ return customResponse;
+ },
+ },
+ {
+ async onRequest() {
+ onRequestCalled = true;
+ return undefined;
+ },
+ },
+ );
+
+ await client.GET("/posts/{id}", { params: { path: { id: 123 } } });
+
+ expect(onRequestCalled).toBe(false);
+});
+
+test("skips onResponse handlers when response is returned from onRequest", async () => {
+ let onResponseCalled = false;
+ const customResponse = Response.json({});
+ const client = createObservedClient();
+
+ client.use({
+ async onRequest() {
+ return customResponse;
+ },
+ async onResponse() {
+ onResponseCalled = true;
+ return undefined;
+ },
+ });
+
+ await client.GET("/posts/{id}", { params: { path: { id: 123 } } });
+
+ expect(onResponseCalled).toBe(false);
+});
From 5a384361796b32fe6563d053c142eb65385528e6 Mon Sep 17 00:00:00 2001
From: Pavel Dubovitsky
Date: Thu, 13 Feb 2025 16:22:19 +0100
Subject: [PATCH 2/2] feat(openapi-fetch): Allow returning Response from
onRequest callback
---
docs/openapi-fetch/middleware-auth.md | 2 +-
.../openapi-fetch/test/middleware/middleware.test.ts | 11 ++++++-----
2 files changed, 7 insertions(+), 6 deletions(-)
diff --git a/docs/openapi-fetch/middleware-auth.md b/docs/openapi-fetch/middleware-auth.md
index 12c0746c4..bf94c6aca 100644
--- a/docs/openapi-fetch/middleware-auth.md
+++ b/docs/openapi-fetch/middleware-auth.md
@@ -84,7 +84,7 @@ const cacheMiddleware: Middleware = {
onResponse({ request, response }) {
if (response.ok) {
const key = getCacheKey(request);
- cache.set(key, response);
+ cache.set(key, response.clone());
}
}
};
diff --git a/packages/openapi-fetch/test/middleware/middleware.test.ts b/packages/openapi-fetch/test/middleware/middleware.test.ts
index d1643fbd3..f215e9cfe 100644
--- a/packages/openapi-fetch/test/middleware/middleware.test.ts
+++ b/packages/openapi-fetch/test/middleware/middleware.test.ts
@@ -446,7 +446,10 @@ test("type error occurs only when neither onRequest nor onResponse is specified"
test("can return response directly from onRequest", async () => {
const customResponse = Response.json({});
- const client = createObservedClient();
+
+ const client = createObservedClient({}, () => {
+ throw new Error("unexpected call to fetch");
+ });
client.use({
async onRequest() {
@@ -463,13 +466,12 @@ test("can return response directly from onRequest", async () => {
test("skips subsequent onRequest handlers when response is returned", async () => {
let onRequestCalled = false;
- const customResponse = Response.json({});
const client = createObservedClient();
client.use(
{
async onRequest() {
- return customResponse;
+ return Response.json({});
},
},
{
@@ -487,12 +489,11 @@ test("skips subsequent onRequest handlers when response is returned", async () =
test("skips onResponse handlers when response is returned from onRequest", async () => {
let onResponseCalled = false;
- const customResponse = Response.json({});
const client = createObservedClient();
client.use({
async onRequest() {
- return customResponse;
+ return Response.json({});
},
async onResponse() {
onResponseCalled = true;