Middleware & Auth
Middleware allows you to modify either the request, response, or both for all fetches as well as handling errors thrown by fetch
. One of the most common usecases is authentication, but can also be used for logging/telemetry, throwing errors, or handling specific edge cases.
Middleware
Each middleware can provide onRequest()
, onResponse()
and onError
callbacks, which can observe and/or mutate requests, responses and fetch
errors.
import createClient from "openapi-fetch";
import type { paths } from "./my-openapi-3-schema"; // generated by openapi-typescript
const myMiddleware: Middleware = {
async onRequest({ request, options }) {
// set "foo" header
request.headers.set("foo", "bar");
return request;
},
async onResponse({ request, response, options }) {
const { body, ...resOptions } = response;
// change status of response
return new Response(body, { ...resOptions, status: 200 });
},
async onError({ error }) {
// wrap errors thrown by fetch
onError({ error }) {
return new Error("Oops, fetch failed", { cause: error });
},
},
};
const client = createClient<paths>({ baseUrl: "https://myapi.dev/v1/" });
// register middleware
client.use(myMiddleware);
TIP
The order in which middleware are registered matters. For requests, onRequest()
will be called in the order registered. For responses, onResponse()
will be called in reverse order. That way the first middleware gets the first “dibs” on requests, and the final control over the end response.
Skipping
If you want to skip the middleware under certain conditions, just return
as early as possible:
onRequest({ schemaPath }) {
if (schemaPath !== "/projects/{project_id}") {
return undefined;
}
// …
}
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.
Throwing
Middleware can also be used to throw an error that fetch()
wouldn’t normally, useful in libraries like TanStack Query:
onResponse({ response }) {
if (!response.ok) {
// Will produce error messages like "https://example.org/api/v1/example: 404 Not Found".
throw new Error(`${response.url}: ${response.status} ${response.statusText}`)
}
}
Error Handling
The onError
callback allows you to handle errors thrown by fetch
. Common errors are TypeError
s which can occur when there is a network or CORS error or DOMException
s when the request is aborted using an AbortController
.
Depending on the return value, onError
can handle errors in three different ways:
Return nothing which means that the error will still be thrown. This is useful for logging.
onError({ error }) {
console.error(error);
return;
},
Return another instance of Error
which is thrown instead of the original error.
onError({ error }) {
return new Error("Oops", { cause: error });
},
Return a new instance of Response
which means that the fetch
call will proceed as successful.
onError({ error }) {
return Response.json({ message: 'nothing to see' });
},
TIP
onError
does not handle error responses with 4xx
or 5xx
HTTP status codes, since these are considered "successful" responses but with a bad status code. In these cases you need to check the response's status property or ok()
method via the onResponse
callback.
Ejecting middleware
To remove middleware, call client.eject(middleware)
:
const myMiddleware = {
// …
};
// register middleware
client.use(myMiddleware);
// remove middleware
client.eject(myMiddleware);
Handling statefulness
Since middleware uses native Request
and Response
instances, it’s important to remember that bodies are stateful. This means:
- Create new instances when modifying (
new Request()
/new Response()
) - Clone when NOT modifying (
res.clone().json()
)
By default, openapi-fetch
will NOT arbitrarily clone requests/responses for performance; it’s up to you to create clean copies.
const myMiddleware: Middleware = {
onResponse({ response }) {
const data = await response.json();
const data = await response.clone().json();
return undefined;
},
};
Auth
This library is unopinionated and can work with any Authorization setup. But here are a few suggestions that may make working with auth easier.
Basic auth
This basic example uses middleware to retrieve the most up-to-date token at every request. In our example, the access token is kept in JavaScript module state, which is safe to do for client applications but should be avoided for server applications.
import createClient, { type Middleware } from "openapi-fetch";
import type { paths } from "./my-openapi-3-schema";
let accessToken: string | undefined = undefined;
const authMiddleware: Middleware = {
async onRequest({ request }) {
// fetch token, if it doesn’t exist
if (!accessToken) {
const authRes = await someAuthFunc();
if (authRes.accessToken) {
accessToken = authRes.accessToken;
} else {
// handle auth error
}
}
// (optional) add logic here to refresh token when it expires
// add Authorization header to every request
request.headers.set("Authorization", `Bearer ${accessToken}`);
return request;
},
};
const client = createClient<paths>({ baseUrl: "https://myapi.dev/v1/" });
client.use(authMiddleware);
const authRequest = await client.GET("/some/auth/url");
Conditional auth
If authorization isn’t needed for certain routes, you could also handle that with middleware:
const UNPROTECTED_ROUTES = ["/v1/login", "/v1/logout", "/v1/public/"];
const authMiddleware = {
onRequest({ schemaPath, request }) {
if (UNPROTECTED_ROUTES.some((pathname) => schemaPath.startsWith(pathname))) {
return undefined; // don’t modify request for certain paths
}
// for all other paths, set Authorization header as expected
request.headers.set("Authorization", `Bearer ${accessToken}`);
return request;
},
};