Features

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:

ParameterTypeDescription
fulfilled(config) => configTransform or inspect the config before the request is sent. Must return the config.
rejected(error) => anyHandle errors thrown by previous interceptors in the chain.
optionsInterceptorOptionsAdditional 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:

  1. Checks the runWhen predicate (if provided) and skips if it returns false
  2. Calls your fulfilled(config) callback
  3. If fulfilled returns a new config, merges it back via Object.assign(config, result)
  4. If fulfilled throws and rejected is provided, passes the error to rejected
  5. If fulfilled throws and no rejected handler 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 fulfilled callback is wrapped as an afterResponse hook
  • The rejected callback is wrapped as a beforeError hook

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:

FeatureInterceptorsHooks
API styleuse/eject/clear patternDirect array push
Removable by IDYes (eject(id))Manual splice
runWhen predicateBuilt-in via optionsImplement manually
Available hooksbeforeRequest, afterResponse, beforeErrorAll 26 hooks
Best forAuth, logging, error handlingLow-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.