Interceptors
Rezo provides an interceptor API through interceptors.request and interceptors.response. Under the hood, interceptors are a convenience layer built on top of Rezo’s hooks system — every interceptor registered via use() is internally pushed into the beforeRequest, afterResponse, or beforeError hooks arrays. This means interceptors and hooks coexist naturally and execute in the same pipeline.
Request Interceptors
Register a request interceptor with interceptors.request.use(). The fulfilled callback receives the full RezoConfig object and must return it (or a promise resolving to it). Use this to modify headers, inject auth tokens, log outgoing requests, or transform the config before it reaches the adapter.
import { Rezo } from 'rezo';
const client = new Rezo();
// Add an authorization header to every request
const id = client.interceptors.request.use(
(config) => {
config.headers.set('Authorization', `Bearer ${getToken()}`);
return config;
},
(error) => {
// Handle errors from previous interceptors in the chain
console.error('Request interceptor error:', error);
return Promise.reject(error);
}
); Signature
interceptors.request.use(
fulfilled?: (config: RezoConfig) => RezoConfig | Promise<RezoConfig>,
rejected?: (error: any) => any,
options?: InterceptorOptions
): number Parameters:
| Parameter | Type | Description |
|---|---|---|
fulfilled | (config) => config | Transform or inspect the config before the request is sent. Must return the config. |
rejected | (error) => any | Handle errors thrown by previous interceptors in the chain. |
options | InterceptorOptions | Additional options such as runWhen predicate. |
Returns: An interceptor ID (number) for use with eject().
How It Works Internally
When you call interceptors.request.use(fulfilled, rejected), Rezo wraps your callback into a beforeRequest hook function and pushes it into hooks.beforeRequest[]. The wrapper:
- Checks the
runWhenpredicate (if provided) and skips if it returnsfalse - Calls your
fulfilled(config)callback - If
fulfilledreturns a new config, merges it back viaObject.assign(config, result) - If
fulfilledthrows andrejectedis provided, passes the error torejected - If
fulfilledthrows and norejectedhandler exists, re-throws the error
Response Interceptors
Register a response interceptor with interceptors.response.use(). The fulfilled callback receives the RezoResponse and must return it. The optional rejected callback handles request errors (network failures, non-2xx statuses when validateStatus rejects them, etc.).
// Transform response data
client.interceptors.response.use(
(response) => {
// Unwrap API envelope
if (response.data?.results) {
response.data = response.data.results;
}
return response;
},
(error) => {
// Handle 401 globally
if (error.response?.status === 401) {
redirectToLogin();
}
return Promise.reject(error);
}
); Signature
interceptors.response.use(
fulfilled?: (response: RezoResponse) => RezoResponse | Promise<RezoResponse>,
rejected?: (error: any) => any,
options?: InterceptorOptions
): number How It Works Internally
Response interceptors push into two separate hook arrays:
- The
fulfilledcallback is wrapped as anafterResponsehook - The
rejectedcallback is wrapped as abeforeErrorhook
This means your response success handler and error handler participate in the normal hook pipeline alongside any hooks you register directly.
Conditional Execution with runWhen
The options.runWhen predicate lets you conditionally execute an interceptor. When runWhen returns false, the interceptor is skipped entirely for that request.
// Only add API key for requests to our own API
client.interceptors.request.use(
(config) => {
config.headers.set('X-API-Key', process.env.API_KEY);
return config;
},
null,
{
runWhen: (config) => config.url.startsWith('https://api.myservice.com')
}
);
// Only log non-cached responses
client.interceptors.response.use(
(response) => {
console.log(`${response.status} ${response.config.url}`);
return response;
},
null,
{
runWhen: (response) => response.status !== 304
}
); InterceptorOptions
interface InterceptorOptions {
/** Only run when this predicate returns true */
runWhen?: (config: any) => boolean;
} Removing Interceptors
eject(id)
Remove a single interceptor by its ID. The ID is returned from the use() call.
const authInterceptor = client.interceptors.request.use((config) => {
config.headers.set('Authorization', `Bearer ${token}`);
return config;
});
// Later: remove it
client.interceptors.request.eject(authInterceptor); When you eject an interceptor, its underlying hook function is spliced out of the hooks array. The interceptor slot is nulled out, so subsequent forEach calls skip it.
clear()
Remove all interceptors registered through this manager. This iterates through every entry and calls eject() on each one, then resets the internal entries array.
// Remove all request interceptors
client.interceptors.request.clear();
// Remove all response interceptors
client.interceptors.response.clear(); Note that clear() only removes interceptors registered via use(). Any hooks registered directly on client.hooks.beforeRequest or client.hooks.afterResponse are unaffected.
Iterating Interceptors
forEach(fn)
Iterate over all active (non-ejected) interceptors. Useful for debugging or introspection.
client.interceptors.request.forEach(({ fulfilled, rejected }) => {
console.log('Request interceptor:', fulfilled?.name || 'anonymous');
});
client.interceptors.response.forEach(({ fulfilled, rejected }) => {
console.log('Response interceptor:', {
hasFulfilled: !!fulfilled,
hasRejected: !!rejected
});
}); Interceptor Count
Both managers expose a size getter that returns the number of currently active interceptors.
console.log(client.interceptors.request.size); // 2
console.log(client.interceptors.response.size); // 1 Common Patterns
Authentication Token Refresh
// Retry with refreshed token on 401
client.interceptors.response.use(
(response) => response,
async (error) => {
const originalConfig = error.config;
if (error.response?.status === 401 && !originalConfig._retried) {
originalConfig._retried = true;
const newToken = await refreshAuthToken();
originalConfig.headers.set('Authorization', `Bearer ${newToken}`);
return client.request(originalConfig);
}
return Promise.reject(error);
}
); Request Timing
client.interceptors.request.use((config) => {
config._startTime = performance.now();
return config;
});
client.interceptors.response.use((response) => {
const duration = performance.now() - response.config._startTime;
console.log(`${response.config.method} ${response.config.url} took ${duration.toFixed(0)}ms`);
return response;
}); Request/Response Logging
client.interceptors.request.use((config) => {
console.log(`--> ${config.method} ${config.url}`);
return config;
});
client.interceptors.response.use(
(response) => {
console.log(`<-- ${response.status} ${response.config.url}`);
return response;
},
(error) => {
console.error(`<-- ERROR ${error.message}`);
return Promise.reject(error);
}
); Interceptors vs Hooks
Both APIs access the same underlying pipeline. Choose based on your use case:
| Feature | Interceptors | Hooks |
|---|---|---|
| API style | use/eject/clear pattern | Direct array push |
| Removable by ID | Yes (eject(id)) | Manual splice |
runWhen predicate | Built-in via options | Implement manually |
| Available hooks | beforeRequest, afterResponse, beforeError | All 26 hooks |
| Best for | Auth, logging, error handling | Low-level events, caching, cookies, proxy |
If you need access to hooks beyond the request/response lifecycle (such as onSocket, onDns, beforeCache, proxy hooks, etc.), use the hooks API directly.