All questions

HTTP Client Interceptors

Premium

HTTP Client Interceptors

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.

Signature

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 };
};

Examples

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' }

Notes

  • Registration order — both chains run in the order handlers were registered: request interceptors transform the config before the adapter, response interceptors transform the response after it.
  • Errors skip forward — if any step throws or rejects, the value skips every later handler until one was registered with an onRejected. That handler either recovers by returning a value or re-throws. If none handles it, request() rejects.
  • Handlers can be async — an 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.
  • The adapter is a stand-in — you do not implement the network call. adapter(config) resolves the raw response, and the tests mock it. Throw a TypeError if it is not a function.

FAQ

In what order do request and response interceptors run?
In this question both chains run in registration order: request interceptors transform the config before the adapter, response interceptors transform the response after it. (Real axios runs request interceptors in reverse registration order — a difference worth noting.)
How does an error skip to the next interceptor?
Each interceptor becomes one .then(onFulfilled, onRejected) in a Promise chain. When a step rejects, the chain walks past every pair whose onRejected is undefined until one handles it, which can recover by returning a value or re-throw.
Why null the ejected slot instead of removing it from the array?
use() returns an id that is the handler's index, so splicing an element out would shift every later id. Nulling the slot keeps the remaining ids valid; the chain builder simply skips null slots.

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.

Upgrade to Premium