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

xhr-inject

v0.1.1

Published

Plugin-based XHR interceptor — header injection, mock responses, queue concurrency, and transparent elevation flows (2FA / transaction signing).

Readme

xhr-hook

A plugin-based XMLHttpRequest interceptor for the browser. Patches window.XMLHttpRequest non-destructively so it chains safely with AppDynamics, Google Analytics, and any other tools that have already patched XHR.

npm install xhr-hook

Features

  • Plugin pipelinebeforeRequest / afterResponse / onError hooks, each async, run in registration order
  • Header injection — mutate or add headers before every request
  • Mock responses — short-circuit the network entirely from a plugin (great for testing / feature flags)
  • Request queue — configurable concurrency, priority, and queue timeout
  • Promise APIinterceptor.fetch() returns Promise<ResponseData>, drop-in as a react-query queryFn
  • Elevation flows — automatic 2FA step-up and transaction signing via the optional elevation plugin
  • Non-conflicting — subclass strategy, never mutates XMLHttpRequest.prototype; compatible with Axios, AppDynamics, GA, and other XHR patchers

Quick start

import { XHRInterceptor } from 'xhr-hook';

const interceptor = new XHRInterceptor();

interceptor.use({
  name: 'auth',
  beforeRequest(config) {
    config.headers['authorization'] = `Bearer ${getToken()}`;
    return config;
  },
});

interceptor.install(); // replaces window.XMLHttpRequest

After install() every XHR fired by the page — including those from Axios, fetch polyfills, and third-party SDKs — flows through the plugin pipeline.

interceptor.fetch()

A Promise-based API compatible with react-query:

// react-query
useQuery({
  queryKey: ['users'],
  queryFn: () => interceptor.fetch({ url: '/api/users' }).then(r => r.body),
});

// plain async/await
const { body, status, duration } = await interceptor.fetch({
  url: '/api/data',
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ key: 'value' }),
});

Axios custom adapter

Route an Axios instance through the interceptor so every Axios call benefits from the plugin pipeline (including transparent elevation):

const axiosInstance = axios.create({
  adapter(config) {
    return interceptor.fetch({
      url:     config.url,
      method:  (config.method || 'GET').toUpperCase(),
      headers: config.headers ?? {},
      body:    config.data ?? null,
    }).then(r => ({
      data: r.body, status: r.status, statusText: r.statusText,
      headers: r.headers, config, request: null,
    }));
  },
});

Plugin API

interface XHRPlugin {
  name: string;
  beforeRequest?(config: RequestConfig): RequestConfig | MockResponse | void | Promise<...>;
  afterResponse?(response: ResponseData, config: RequestConfig): ResponseData | void | Promise<...>;
  onError?(error: XHRError, config: RequestConfig): ResponseData | void | Promise<...>;
}

| Hook | Purpose | |------|---------| | beforeRequest | Mutate URL, method, headers, body, timeout. Return a MockResponse to skip the network entirely. | | afterResponse | Inspect or transform the response. Return a new ResponseData to replace it. | | onError | Recover from network / timeout / abort errors by returning a synthetic ResponseData. |

Mock responses

import { mockResponse } from 'xhr-hook';

interceptor.use({
  name: 'feature-flag-mock',
  beforeRequest(config) {
    if (config.url.includes('/api/beta')) {
      return mockResponse({ status: 200, body: { enabled: true }, delay: 50 });
    }
  },
});

Queue concurrency

const interceptor = new XHRInterceptor({
  queue: {
    concurrency: 2,      // max 2 in-flight at once
    timeout: 10_000,     // reject after 10 s in queue
    priorityFn: (a, b) => (b.config.metadata.priority ?? 0) - (a.config.metadata.priority ?? 0),
  },
});

Elevation plugin

The optional elevation plugin handles 2FA step-up auth and transaction signing transparently. When a response signals that elevation is required, the plugin runs the challenge, patches the headers, and retries — the original caller receives the final successful response as if nothing happened.

import { XHRInterceptor } from 'xhr-hook';
import {
  createElevationPlugin,
  createAuthElevationHandler,
  createTransactionElevationHandler,
  createTokenStore,
} from 'xhr-hook/elevation';

2FA / step-up auth

const tokenStore = createTokenStore(localStorage.getItem('token') ?? '');

// Inject current token on every request
interceptor.use({
  name: 'token-injector',
  beforeRequest(config) {
    config.headers['authorization'] = `Bearer ${tokenStore.get()}`;
    return config;
  },
});

const authHandler = createAuthElevationHandler({
  // Detect the elevation signal however your API expresses it
  matches: (r) => r.body?.code === 'ELEVATION_REQUIRED' && r.body?.type === '2FA',

  // Show your 2FA UI, resolve with the new token string
  elevate: async () => {
    const { token } = await show2FAModal();
    return token;
  },

  // Persist the refreshed token so queued requests pick it up automatically
  onTokenRefreshed: (token) => {
    tokenStore.set(token);
    localStorage.setItem('token', token);
  },
});

interceptor.use(createElevationPlugin(interceptor, {
  handlers: [authHandler],
  retryTimeout: 0,   // no hard timeout on retries — user may take time in the modal
}));

Concurrent requests that all hit the elevation signal are deduplicated: elevate() is called exactly once, and all waiting requests retry with the same new token.

Transaction signing

const txHandler = createTransactionElevationHandler({
  matches: (r) => r.body?.code === 'ELEVATION_REQUIRED' && r.body?.type === 'TRANSACTION',

  // Called independently per request — each gets its own one-time token
  sign: async (config) => {
    const { 'x-tx-token': token } = await callSigningService(config);
    return { 'x-tx-token': token };
  },
});

retryTimeout

Override the XHR timeout specifically for retry requests. Applied in beforeRequest after send() has snapshotted the caller's timeout, so it reliably overrides any Axios / library-set timeout:

createElevationPlugin(interceptor, {
  handlers: [...],
  retryTimeout: 0,        // 0 = no timeout on retries
  // retryTimeout: 5000   // or a specific ms value
})

API reference

new XHRInterceptor(options?)

| Option | Type | Default | Description | |--------|------|---------|-------------| | queue.concurrency | number | Infinity | Max simultaneous in-flight requests | | queue.timeout | number | — | Max ms a request may wait in queue | | queue.priorityFn | function | — | Custom sort for the queue | | rejectOnHttpError | boolean | true | Throw XHRError for HTTP >= 400 |

interceptor.use(plugin) / .remove(name)

Register or remove a plugin. Plugins run in registration order.

interceptor.install() / .uninstall()

Replace / restore window.XMLHttpRequest. Captures whatever window.XMLHttpRequest is at call time, so it composes correctly with other patchers.

interceptor.fetch(partial)

Fire a request through the interceptor and return Promise<ResponseData>.

mockResponse(partial)

Helper to create a MockResponse from a plugin's beforeRequest.

Development

bun run test     # run all tests (vitest, jsdom)
bun run build    # build dist/ for npm
bun run demo     # start demo server at http://localhost:3000

License

MIT