An interceptor is a function that runs automatically on every request or response as it passes through an HTTP client, letting you transform the outgoing config before it is sent or the incoming response before your code sees it. This is the mechanism behind axios' interceptors.request.use and interceptors.response.use. Build the machine behind them: a client that threads each call through a chain of registered handlers, runs the network adapter in the middle, and handles failures axios-style.
type Handler = (value: any) => any | Promise<any>; // receives, returns the next value
type ErrorHandler = (error: any) => any; // recover (return) or re-throw
type Manager = {
use(onFulfilled?: Handler, onRejected?: ErrorHandler): number; // returns an id
eject(id: number): void;
};
function httpClientInterceptors(adapter: (config: any) => Promise<any>): {
request(config: any): Promise<any>;
interceptors: { request: Manager; response: Manager };
};
A request interceptor transforms the config before the adapter (the network call) receives it:
const client = httpClientInterceptors(async (config) => ({ status: 200, config }));
client.interceptors.request.use((config) => ({
...config,
headers: { ...config.headers, Authorization: 'Bearer token' },
}));
await client.request({ url: '/me' });
// the adapter received { url: '/me', headers: { Authorization: 'Bearer token' } }
A response error handler recovers from a rejected adapter, so request() resolves instead of rejecting:
const client = httpClientInterceptors(() => Promise.reject(new Error('503')));
client.interceptors.response.use(
(res) => res,
(err) => ({ status: 'recovered', reason: err.message }),
);
await client.request({ url: '/flaky' });
// resolves with { status: 'recovered', reason: '503' }
onRejected. That handler either recovers by returning a value or re-throws. If none handles it, request() rejects.onFulfilled may return a Promise (for example, refreshing a token mid-flight). The next stage waits for it to settle.use returns an id, eject removes it — ejecting one handler must not disturb the ids already handed out for the others.adapter(config) resolves the raw response, and the tests mock it. Throw a TypeError if it is not a function.Unlock the solution & editor
Runnable editor + tests
Solve in the browser with instant Jest feedback.
Detailed solutions
Walkthroughs, edge cases, and complexity notes.
Multi-framework variants
React, Vue, Vanilla, Angular — same question, different stacks.