valve_

HTTP clients

Route fetch, undici, axios, and got through a valve, with provider feedback.

valve works with any client. What differs is how it sees the response, which it needs to read rate limit headers.

All snippets assume a valve like this one:

import { createValve } from 'valvejs';
import { redisStore } from 'valvejs/redis';

const partner = createValve({
  id: 'partner:key-1',
  rate: { limit: 50, interval: '1s' },
  store: redisStore({ client: redis }),
});

fetch

wrapFetch gives you a fetch with the same signature. It observes every response and retries 429s for idempotent methods by default. POST and PATCH require explicit opt-in.

const partnerFetch = partner.wrapFetch();

const res = await partnerFetch('https://api.partner.example/v2/orders', {
  headers: { authorization: `Bearer ${token}` },
});

Assign a cost or priority from the request:

const partnerFetch = partner.wrapFetch(fetch, {
  cost: (input) => (String(input).includes('/search') ? 5 : 1),
  priority: (_input, init) => (init?.method === 'GET' ? 1 : 0),
});

undici

Pass undici's fetch as the implementation.

import { fetch as undiciFetch } from 'undici';

const partnerFetch = partner.wrapFetch(undiciFetch, { retries: 3 });

axios

axios rejects non-2xx responses by default, and the observe run option only sees resolved results. Observe the error's response too, so a 429 still pauses every worker.

axios header types do not match valve's HeadersLike, so map the response with a small helper. At runtime, axios headers support case-insensitive get, which valve uses.

import axios, { isAxiosError, type AxiosResponse } from 'axios';
import type { ResponseLike } from 'valvejs';

const fromAxios = (res: AxiosResponse): ResponseLike => ({
  status: res.status,
  headers: res.headers as Record<string, string>,
});

async function partnerGet<T>(url: string) {
  return partner.run(async ({ signal }) => {
    try {
      const res = await axios.get<T>(url, { signal });
      partner.observe(fromAxios(res));
      return res.data;
    } catch (error) {
      if (isAxiosError(error) && error.response)
        partner.observe(fromAxios(error.response));
      throw error;
    }
  });
}

To let a 429 resolve instead, set validateStatus and use the observe option:

const res = await partner.run(
  ({ signal }) => axios.get(url, { signal, validateStatus: () => true }),
  { observe: fromAxios },
);

got

got exposes statusCode instead of status, so map it.

import got from 'got';

const res = await partner.run(
  ({ signal }) => got(url, { signal, throwHttpErrors: false }),
  { observe: (r) => ({ status: r.statusCode, headers: r.headers }) },
);

Reusable wrappers

wrap turns any async function into a rate limited one with the same parameters. It is useful for SDK methods.

const getOrder = partner.wrap((id: string) => sdk.orders.get(id), {
  priority: 1,
});

const order = await getOrder('ord_123');

wrap does not see the HTTP response, so pair it with partner.observe(...) inside the function if the SDK exposes headers.

On this page