@samveys/http-client

Version
1.0.3Nov 5, 2026
Install
pnpm add @samveys/http-client

From packages/http-client/README.md in the framework repository.

An HTTP client over fetch-style requests with JSON helpers, timeouts, retries, a concurrency pool, test fakes and SSRF protection for URLs that come from users. No runtime dependencies.

Make requests

ts
import { createHttpClient } from '@samveys/http-client'

const client = createHttpClient({
  baseUrl: 'https://api.example.com/v1',
  timeoutMs: 5_000, // per attempt, via AbortSignal.timeout
  retries: { count: 2, backoffMs: [100, 400], on: [502, 503, 504] },
  concurrency: 4, // at most 4 requests in flight; the rest wait their turn
})

export async function loadUser(id: number) {
  return client.json<{ id: number; name: string }>(`/users/${id}`, { query: { expand: 'profile' } })
}

export async function createUser(name: string) {
  const response = await client.post('/users', { json: { name } })
  return response.status
}
  • client.json() parses a 2xx JSON answer and throws SVS_E107 for any other status.
  • A request that still fails after the last retry (timeout, network error, or a status in on) is SVS_E107 (502).
  • retries and timeoutMs can be overridden per request.

Never fetch a user-supplied URL directly (SSRF)

A URL typed by a user (an avatar link, a webhook target, an import source) can point at your own network. Use userUrl() (or untrusted: true) and the client:

  1. accepts only http: and https: and no credentials in the URL;
  2. resolves the host with node:dns/promises (lookup with all) and refuses the request if any address is loopback, private (10/8, 172.16/12, 192.168/16), carrier-grade NAT (100.64/10), link-local (169.254/16, which includes the cloud metadata address 169.254.169.254), IPv6 ::1, fc00::/7, fe80::/10, an IPv4-mapped form such as ::ffff:127.0.0.1, or 0.0.0.0;
  3. connects to the address it vetted, so DNS cannot change between the check and the connection (no DNS rebinding);
  4. follows redirects itself and vets every hop, so a public URL that redirects to http://10.0.0.1/ is refused;
  5. drops Authorization and Cookie when a redirect leaves the origin.

A refusal is SVS_E106 (502).

ts
import { createHttpClient } from '@samveys/http-client'

const client = createHttpClient({ timeoutMs: 3_000 })

export async function fetchAvatar(url: string): Promise<Uint8Array> {
  const response = await client.userUrl(url) // SVS_E106 for 169.254.169.254, localhost, private ranges…
  return new Uint8Array(await response.arrayBuffer())
}

Fake requests in tests

ts
import { createHttpClient } from '@samveys/http-client'

export async function exampleTest() {
  const client = createHttpClient()
  client
    .fake({
      'https://api.x/users/*': { json: { id: 7 } },
      'https://api.x/health': { status: 204 },
      'https://api.x/echo': (request) => new Response(request.method),
    })
    .preventStrayRequests()

  const user = await client.json('https://api.x/users/7') // { id: 7 }, no network
  await client.get('https://unfaked.example/') // throws SVS_E106: the request was not faked
  return { user, sent: client.recorded() }
}

A * in a pattern matches any run of characters. recorded() lists every request the client handled (method, URL, headers, body, whether it was faked) for assertions.

Reference

ts
createHttpClient({ baseUrl?, timeoutMs?, retries?: { count, backoffMs[], on[] }, concurrency?, headers?, untrusted?, maxRedirects?, resolver?, transport? })
client.request | get | post | put | patch | delete (url, { query?, json?, body?, headers?, timeoutMs?, retries?, untrusted?, signal? })
client.json<T>(url, options?)   client.userUrl(url, options?)
client.fake(table)   client.preventStrayRequests()   client.allowStrayRequests()   client.recorded()