Centralizing Auth with Axios Request and Response Interceptors
Scattered Authorization headers and repeated 401 handling create fragile code. Using axios request and response interceptors on a custom instance centralizes token attachment and single‑refresh retry logic, with ordering caveats and verification steps.
12 Aug 2026, 22:27 UTC

The real pain isn’t making a request with axios. It’s the same Authorization header being set in dozens of call sites, and 401 handling being re‑implemented everywhere a token can expire. When the refresh flow changes, the fix spreads across the codebase. A single axios instance with a request interceptor and a response interceptor moves those cross‑cutting concerns to the client boundary.
Scattered headers and ad‑hoc 401s
In a typical service layer you see config.headers.Authorization being set manually, sometimes forgotten, sometimes duplicated. A 401 is caught per call, with some places redirecting, some retrying, some swallowing. The bug surface grows with every new endpoint.
Interceptors are functions registered with axios.interceptors.request.use and axios.interceptors.response.use. A request interceptor runs before the request is sent and can mutate config. A response interceptor runs on success and on error before the caller sees the promise. Attaching them to an instance created by axios.create keeps them scoped to that client.
One instance, one place for auth
Create a dedicated client rather than using the global axios module.
// client.js – run in Node or browser bundle, no elevated permissions required
import axios from 'axios';
const api = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000 // axios default is 0 meaning no timeout
});
The request interceptor reads an access token from storage and attaches it. In axios 1.x config.headers is an AxiosHeaders instance with set/get methods, not a plain object.
api.interceptors.request.use(config => {
const token = getAccessToken(); // placeholder for your storage read
if (token) {
config.headers.set('Authorization', `Bearer ${token}`);
}
return config;
});
request.use returns an id you can pass to api.interceptors.request.eject(id) to remove the interceptor later. Keeping interceptors on the created instance means other code importing axios is unaffected.
Worked example: single refresh with dedupe
The response interceptor’s rejection handler can detect 401, refresh once, and replay the original request. A shared in‑flight refresh promise prevents a storm of refresh calls when multiple requests fail together.
let refreshPromise = null;
api.interceptors.response.use(
response => response,
async error => {
const original = error.config;
if (error.response?.status === 401 && !original._retry) {
original._retry = true;
if (!refreshPromise) {
refreshPromise = refreshAccessToken().finally(() => {
refreshPromise = null;
});
}
await refreshPromise;
// request interceptor runs again on retry and re‑attaches fresh token
return api(original);
}
return Promise.reject(error);
}
);
Marking the retry with original._retry avoids an infinite loop if the refreshed token is still rejected. The replayed request re‑enters the interceptor pipeline, so the request interceptor runs again and sets the new Authorization header.
Token storage in this sketch is simplified. Memory, localStorage, or cookies each have XSS and CSRF implications. Treat the storage helper as a placeholder for your security‑reviewed mechanism.
Ordering and error‑swallowing trade‑offs
Axios interceptor ordering is not intuitive. Request interceptors run in reverse of the order they were added, while response interceptors run in the order added. If you rely on sequencing, verify this in your pinned version with two logging interceptors.
A rejection handler that returns normally swallows the error and makes failures look like successes. Always rethrow or return Promise.reject(error) after handling, otherwise callers will receive undefined as a resolved value.
Version assumptions matter. The header API and interceptor behavior changed between the 0.x and 1.x major lines. Pin the axios version you target and check the README for request.use, response.use, and eject signatures before copying snippets.
Limitations and how to verify
Interceptors centralize logic but add implicit behavior. New team members may not expect a retry to happen inside the client. Document the contract: which errors are retried, how many times, and where tokens live.
Practical checks:
- Register two request interceptors that log a label and confirm the reverse execution order for requests and forward order for responses.
- Mock an endpoint that returns 401 once then 200, fire three parallel requests, and confirm only one refresh call occurs and all three eventually succeed.
- Inspect the ejected interceptor id and confirm
api.interceptors.request.ejectremoves it without affecting other interceptors.
Interceptors give ordering, ejection, and per‑instance scoping out of the box and compose with axios defaults like JSON serialization and params handling. A fetch wrapper is dependency‑free and fully explicit; it forces you to re‑implement the same cross‑cutting concerns. Centralizing auth at the client boundary is the decision; interceptors are the mechanism axios provides for it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.