npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ssi-lib/interceptor

v1.2.1

Published

Interceptor Axios modular: init único para headers, strip CORS, refresh 401 y retry. Desacoplado de React.

Readme

@ssi-lib/interceptor

Interceptor Axios modular: un initInterceptor por app cubre headers, strip CORS, refresh en 401 y retry. Sin acoplarse a React (host, remotes, Node).

Modelo mental

initInterceptor({ axios, getAccessToken, headers: { 'x-tenant-id': fn, 'x-trace-id': fn }, refreshAccessToken, onAuthFailure })
  │
  ├─ request  ──▶ Bearer + headers (mapa llave → valor por request)
  │
  └─ response ──▶ 401 → refresh (single-flight) → retry 1 vez
                  2xx → onResponse?
                  otros errores → onResponseError (notifica, no traga)

El Host no reimplementa interceptores: instala la lib, llama init con providers de sesión, y axios.create() hereda lo mismo.

onResponseError sigue sin renderizar UI: solo notifica. El 401 con refresh configurado no dispara ese callback (lo maneja onAuthFailure si el refresh falla).


Requisitos

  1. Instalar el paquete:
# registry
npm i @ssi-lib/interceptor

# tarball local
npm run pack:local
npm i ./ssi-lib-interceptor-<ver>.tgz
  1. Tener axios (>=0.21.0 || ^1.0.0) como peer dependency.
  2. Usar una instancia de Axios (axios.create(...) o la global) — o createApiClient si preferís factory.

1. Host / app — initInterceptor (recomendado)

Un init en bootstrap, antes de cargar remotes y antes de axios.create() en facades. La lib adjunta el default axios (remotes export const api = axios) y envuelve axios.create().

import axios from 'axios';
import { initInterceptor } from '@ssi-lib/interceptor';

initInterceptor({
  axios,
  getAccessToken: () => readPersistedAuthToken(),
  headers: {
    'x-tenant-id': () => readSessionTenantContext().tenantId,
    'x-trace-id': () => peekOrderId() ?? Date.now(),
  },
  refreshAccessToken: () => refreshPersistedSession(), // fetch al IdP, no axios
  onAuthFailure: () => forceLogoutToLogin(),
});

Después de eso:

  • Host facades: axios.create({ baseURL }) — sin attachInterceptor
  • Remotes: export const api = axios — sin interceptores ni headers de auth
  • 401 → un refresh compartido → retry una vez → si falla, onAuthFailure

refreshAccessToken debe usar fetch (u otro cliente), no la instancia axios interceptada.

Valores de header extra: literal, null/undefined (no se envía), o función sync/async. Para un cliente suelto sin init, sigue existiendo attachInterceptor.


2. Manejo centralizado de errores (onResponseError)

import axios from 'axios';
import { attachInterceptor, type ErrorContext } from '@ssi-lib/interceptor';
import { openGlobalErrorDialog } from './ui/dialogService';

export const api = axios.create({ baseURL: 'https://api.tuempresa.com' });

attachInterceptor(api, {
  headers: {
    'client-id': 'portal-mfe',
  },

  onResponseError: (_error, context: ErrorContext) => {
    console.error(
      `[API Error] ${context.method} ${context.url} (${context.status}):`,
      context.message
    );

    if (context.isNetworkError) {
      openGlobalErrorDialog({
        title: 'Error de Conexión',
        description: 'No pudimos conectar con los servidores.',
      });
      return;
    }

    if (context.isTimeout) {
      openGlobalErrorDialog({
        title: 'Tiempo de Espera Agotado',
        description: 'La solicitud tardó demasiado. Intentá de nuevo.',
      });
      return;
    }

    if (context.isCanceled) {
      return; // abort / CancelToken — sin UI
    }

    openGlobalErrorDialog({
      title: `Error (${context.status ?? 'Desconocido'})`,
      description: context.message,
    });
  },

  errorConfig: {
    // No abrir diálogo si un 404 es esperado por el flujo
    excludeStatusCodes: [404],
    // Endpoints silenciosos / telemetría
    excludeUrls: ['/api/telemetry', /\/health\/.*/],
    // Parser custom del payload del backend
    extractErrorMessage: (axiosError) => {
      const data = axiosError.response?.data as { customError?: string };
      return data?.customError || 'Ocurrió un error inesperado';
    },
  },
});

El error de Axios siempre se re-rechaza (Promise.reject): el callback no lo traga; solo notifica.


3. Filtros de URL en request (includeUrls / excludeUrls)

Solo inyectá headers en rutas que te interesan:

attachInterceptor(api, {
  headers: {
    Authorization: async () => `Bearer ${await getAuthToken()}`,
  },
  // Solo estas URLs reciben headers
  includeUrls: ['/api/', /\/v1\/orders/],
  // Nunca en login / APIs de terceros
  excludeUrls: ['/auth/login', 'https://cdn.terceros.com'],
});

excludeUrls gana sobre includeUrls cuando ambas coinciden.


4. Habilitar / deshabilitar dinámicamente (enabled)

attachInterceptor(api, {
  headers: { 'x-tenant-id': () => getTenantId() },
  enabled: (config) => config.method?.toUpperCase() !== 'OPTIONS',
  // o: enabled: false  — apaga el request interceptor
});

5. Respuestas exitosas (onResponse)

Útil para logging, métricas o telemetría:

attachInterceptor(api, {
  onResponse: (response) => {
    console.log(
      `[HTTP 2xx] ${response.config.method?.toUpperCase()} ${response.config.url} → ${response.status}`
    );
  },
});

6. Factory: cliente Axios preconfigurado (createApiClient)

import { createApiClient } from '@ssi-lib/interceptor';

export const apiClient = createApiClient({
  axiosConfig: {
    baseURL: 'https://api.tuempresa.com/v1',
    timeout: 15000,
  },
  interceptorConfig: {
    headers: {
      'x-tenant-id': 'tenant-acme',
      'client-id': 'portal-proveedores',
    },
    onResponseError: (_error, context) => {
      console.error(context.message, context);
    },
  },
});

// Uso normal de Axios
await apiClient.get('/orders');

7. Helpers de headers

import {
  attachInterceptor,
  createBearerAuthHeader,
  createClientIdHeader,
  createCorrelationIdHeader,
  createTenantHeader,
} from '@ssi-lib/interceptor';

attachInterceptor(api, {
  headers: {
    ...createTenantHeader(() => getActiveTenantId()),           // x-tenant-id
    ...createClientIdHeader('bmad-customer-mfe'),                 // client-id
    ...createBearerAuthHeader(async () => await getSessionToken()), // Authorization: Bearer …
    ...createCorrelationIdHeader(),                              // x-correlation-id (UUID por request)
  },
});

Cada helper acepta un segundo argumento para renombrar el header:

createTenantHeader(getTenantId, 'X-Org-Id');
createBearerAuthHeader(getToken, 'X-Access-Token');
createCorrelationIdHeader(() => myTraceId(), 'X-Request-Id');

createBearerAuthHeader antepone Bearer si el token no lo trae.


8. Headers factory (mapa completo por request)

En lugar de un diccionario fijo, una función que arma todos los headers:

attachInterceptor(api, {
  headers: async (config) => ({
    'client-id': 'portal-mfe',
    'x-tenant-id': getTenantId(),
    Authorization: config.url?.includes('/public')
      ? undefined
      : `Bearer ${await getAuthToken()}`,
  }),
});

9. Error al resolver headers (onError)

Si falla la resolución de un header (provider que lanza), podés capturarlo sin romper el wiring del interceptor de response:

attachInterceptor(api, {
  headers: {
    Authorization: async () => {
      throw new Error('token store unavailable');
    },
  },
  onError: (error, config) => {
    console.error('[headers] falló resolución', config.url, error);
  },
});

La petición se rechaza igual; onError solo notifica.


10. Desconexión (eject)

const handle = attachInterceptor(api, {
  headers: { 'client-id': 'portal-mfe' },
  onResponseError: (_e, ctx) => console.error(ctx.message),
});

// Quita request + response interceptors
handle.eject();

Alias de compatibilidad: attachHeaderInterceptor ≡ attachInterceptor.


11. Completo — host MFE típico

import axios from 'axios';
import { initInterceptor, type ErrorContext } from '@ssi-lib/interceptor';

initInterceptor({
  axios,
  getAccessToken: () => sessionStorage.getItem('access_token'),
  getTenantId: () => sessionStorage.getItem('tenant_id'),
  refreshAccessToken: () => refreshSession(),
  onAuthFailure: () => {
    sessionStorage.clear();
    window.location.replace('/login');
  },
  stripHeaders: ['language', 'tenantId', 'tenantid'],
  excludeUrls: ['/auth/login', '/auth/refresh', /\/health/],
  onResponseError: (_error, context: ErrorContext) => {
    if (context.isCanceled) return;
    window.dispatchEvent(
      new CustomEvent('app:api-error', { detail: context })
    );
  },
  errorConfig: {
    excludeUrls: [/\/telemetry/],
  },
});

API

Exports

| Export | Rol | |--------|-----| | initInterceptor(options) | Init único: default axios + create(), headers, strip, 401 retry | | attachInterceptor(instance, config) | Conecta request + response a una instancia | | attachHeaderInterceptor | Alias de attachInterceptor | | createApiClient(options) | axios.create + attachInterceptor | | createTenantHeader / createClientIdHeader / createBearerAuthHeader / createCorrelationIdHeader / createTraceIdHeader | Helpers de headers | | resolveHeaders / matchUrl / shouldInterceptUrl | Utilidades de request | | buildErrorContext / shouldHandleResponseError / extractDefaultErrorMessage | Utilidades de error | | isAxiosNetworkError / isAxiosTimeout / isAxiosCancel | Detectores | | Tipos | InterceptorConfig, ErrorContext, InterceptorHandle, … |

InterceptorConfig

| Campo | Tipo | Descripción | |-------|------|-------------| | headers? | HeaderDictionary \| HeadersFactory | Headers estáticos/dinámicos a inyectar | | includeUrls? | UrlPattern[] | Solo estas URLs reciben headers | | excludeUrls? | UrlPattern[] | Estas URLs no reciben headers | | enabled? | boolean \| (config) => boolean \| Promise<boolean> | Apaga el request interceptor (default true) | | onError? | (error, config) => void | Fallo al resolver headers | | onResponse? | (response) => void \| Promise<void> | Respuestas 2xx | | onResponseError? | (error, context) => void \| Promise<void> | Errores HTTP / red / timeout | | errorConfig? | ErrorInterceptorConfig | Filtros y parser de mensaje | | stripHeaders? | string[] | Headers a borrar antes de enviar | | refreshAccessToken? | () => token \| Promise<token> | Refresh + retry en 401 (single-flight) | | onAuthFailure? | () => void | Refresh falló o no hay token | | unauthorizedStatus? | number | Status que dispara refresh (default 401) |

ErrorInterceptorConfig

| Campo | Descripción | |-------|-------------| | excludeStatusCodes? | Códigos que no disparan onResponseError | | excludeUrls? | URLs/patrones que no disparan onResponseError | | extractErrorMessage? | Parser custom del mensaje desde AxiosError |

ErrorContext

| Propiedad | Tipo | Descripción | |-----------|------|-------------| | status | number \| undefined | HTTP status (undefined si falló la red) | | statusText | string \| undefined | Texto del status | | url | string \| undefined | URL de la petición | | method | string \| undefined | Método en mayúsculas | | message | string | Mensaje normalizado | | data | unknown | response.data del error | | isNetworkError | boolean | Fallo de conectividad | | isTimeout | boolean | Timeout superado | | isCanceled | boolean | Abort / CancelToken | | config | InternalAxiosRequestConfig | Config de Axios |

InterceptorHandle

| Campo / método | Descripción | |----------------|-------------| | eject() | Remueve interceptors de request y response | | instance | Instancia de Axios usada | | requestInterceptorId / responseInterceptorId | IDs internos de Axios | | interceptorId | Compat: request id o response id |

CreateApiClientOptions

| Campo | Descripción | |-------|-------------| | axiosConfig? | CreateAxiosDefaults (baseURL, timeout, …) | | interceptorConfig | InterceptorConfig (requerido) |


Build

npm install
npm run build      # dist/ (ESM + CJS + .d.ts)
npm run typecheck
npm test
npm run pack:local # ssi-lib-interceptor-<ver>.tgz