Http Client
Outbound HTTP client with SSRF gate, retry, circuit breaker, wall-clock + idle timeouts, AbortSignal propagation, connection pooling, streaming, and ALPN-negotiated HTTP/2. Built on node:http, node:https, and node:http2 with zero npm runtime dependency.
Every outbound request flows through b.ssrfGuard out of the box: hostname → DNS lookup is pinned to vetted IP literals, RFC 1918 / loopback / link-local / IPv6 ULA destinations are refused, and the redirect chain is re-validated at every hop so a 302 to http://169.254.169.254/ (cloud metadata) can't smuggle past the first-hop gate. The same DNS pinning applies to retries — there's no retry path that bypasses the guard.
Protocol selection is automatic. HTTPS origins handshake with ALPN ['h2', 'http/1.1'] and cache the resulting transport per . While a transport is mid-negotiate the cache holds the in-flight Promise so concurrent calls to a new origin coalesce onto a single connection. h2 GOAWAY or session error evicts the entry; the next request reconnects.
Resiliency defaults: TLS 1.3 minimum, PQC-preferred ecdhCurve group order, split wall-clock vs zero-progress idle timeouts, request-body stream errors propagated to the returned Promise, and h2 stream cancellation via NGHTTP2_CANCEL (clean, not stream.destroy) when the AbortSignal fires.
b.httpClient.cache.memoryStore(opts) #
{
maxBytes: number, // total stored body bytes; default: 64 MiB
maxEntries: number, // count cap; default: 1024
evictionPolicy: "lru", // currently the only policy; reserved
}
In-memory bounded-LRU cache store implementing the Store shape: get(key), set(key, entry), delete(key), clear(). Eviction runs when the byte total or entry count exceeds the configured caps; eviction emits an audit event when an audit sink is wired (via b.httpClient.cache.create({ audit })). Stored values include the response body buffer, so the byte total reflects real memory pressure rather than a rough estimate.
Suitable for single-process workloads. For shared-cache semantics across a fleet, wire your own Store against a shared backing service (Redis, filesystem, etc.) — the same shape applies.
var store = b.httpClient.cache.memoryStore({
maxBytes: 16 * 1024 * 1024,
maxEntries: 256,
});
var cache = b.httpClient.cache.create({ store: store });
await b.httpClient.request({ url: "https://example.com/", cache: cache });
b.httpClient.cache.create(opts) #
{
store: , // Store: { get, set, delete, clear }
sharedCache: true, // honour s-maxage; refuse Cache-Control: private
defaultMaxStale: 0, // seconds — serve stale up to this far past expiry
revalidateInBackground: true, // s-w-r kicks off background revalidation
audit: undefined, // audit sink with safeEmit({...})
observability: undefined, // optional { event, safeEvent }
statusHeader: "x-blamejs-cache", // response header carrying the cache decision; null/false to suppress, or a custom name (e.g. "x-cache")
}
Builds an RFC 9111 cache instance for b.httpClient.request. The returned object plugs into a request via opts.cache. Without opts.cache, the request path is unchanged — no overhead for non-caching callers. The cache evaluates each response per RFC 9111 §3 (storage decision: method / status / Cache-Control / Vary), tracks freshness per §4.2 (s-maxage > max-age > Expires > heuristic 10% of (Date - Last-Modified) capped at 24h), revalidates conditionally per §4.3 (If-None-Match / If-Modified-Since), and merges 304 headers per §5.
sharedCache: true (default) honours s-maxage over max-age and refuses to store responses with Cache-Control: private — operator services share a cache with each other, so a per-user private response must not leak across users via the cache. Single-tenant scripts pass sharedCache: false to behave as a private cache.
defaultMaxStale lets the cache return a stored entry past its freshness lifetime (within the configured number of seconds) even without an explicit upstream stale-while-revalidate / stale-if-error. Default 0 — operators opt in.
revalidateInBackground (default true): when an entry is fresh within its stale-while-revalidate window the stale response is returned immediately and a background revalidation kicks off so the next caller sees a refreshed entry. Pass false to revalidate inline (lower memory churn, higher request latency).
var cache = b.httpClient.cache.create({
store: b.httpClient.cache.memoryStore({ maxBytes: 32 * 1024 * 1024 }),
sharedCache: true,
defaultMaxStale: 5,
audit: b.audit,
});
var res = await b.httpClient.request({
url: "https://api.example.com/users/42",
cache: cache,
});
// res.headers["x-blamejs-cache"] === "MISS" (first call)
b.httpClient.configurePool(opts) #
{
keepAlive: true, // boolean — whether to reuse sockets
keepAliveMsecs: 1000, // positive integer ms between keep-alive probes
maxSockets: 16, // positive integer — concurrent sockets per origin
maxFreeSockets: 8, // positive integer — idle sockets retained per origin
scheduling: "lifo", // "lifo" | "fifo"
}
Updates the keepAlive Agent options used for new h1 transports and tears down the per-origin transport cache so subsequent requests pick up the fresh values. Existing in-flight responses keep their old transport. Throws on unknown keys, non-positive integers, or a non-boolean keepAlive. Use at boot when the default 16/8 socket caps don't match the operator's downstream concurrency budget.
b.httpClient.configurePool({ maxSockets: 64, maxFreeSockets: 32 });
// → undefined (cache cleared; next request builds a 64-socket pool)
b.httpClient.request(opts) #
{
method: "GET", // HTTP method
url: , // string or URL — destination
headers: {}, // request headers
body: undefined, // Buffer | string | Readable | undefined
timeoutMs: undefined, // wall-clock cap; no default — operator chooses
idleTimeoutMs: 30000, // zero-progress cap
responseMode: "buffer", // "buffer" | "stream" | "always-resolve"
maxResponseBytes: undefined, // 16 MiB control / 1 GiB GET defaults; ignored in "stream"
onChunk: undefined, // (chunk: Buffer) => void — fires per response chunk
maxBytesPerSec: undefined, // token-bucket bandwidth cap (bytes/sec) — paces BOTH the download response and the upload body with backpressure
downloadTransform: undefined, // Transform | () => Transform | array — interpose on the response stream (e.g. a hashing or progress Transform)
uploadTransform: undefined, // Transform | () => Transform | array — interpose on the request body before the wire
signal: undefined, // AbortSignal — propagated to req / stream
errorClass: HttpClientError, // FrameworkError subclass for thrown errors
observer: undefined, // (stage, info) => void — lifecycle hook
agent: undefined, // override per-origin Agent (h1 only)
preferH2: false, // attempt h2c against an HTTP origin (no ALPN)
before: undefined, // array of (opts) => opts | Promise — request mutators
after: undefined, // array of (response) => response | Promise — response mutators
onUploadProgress: undefined, // (bytesSent, totalBytes?) => void
}
Promise-returning, AbortSignal-aware HTTP request. Negotiates h2 / h1 per-origin via ALPN, reuses transports from the cache, runs every destination through b.ssrfGuard before connecting, and re-validates each redirect hop. Returns { statusCode, headers, body } for the default "buffer" mode; "stream" returns a Readable for the body. Sensitive headers (Authorization / Cookie / Proxy-Authorization) are stripped on cross-origin redirect. Body-stream errors propagate to the rejected Promise.
var res = await b.httpClient.request({
method: "GET",
url: "https://example.com/health",
timeoutMs: 5000,
});
// → { statusCode: 200, headers: { "content-type": "application/json", ... }, body: }
b.httpClient.downloadStream(opts) #
{
url: , // string — source
dest: , // absolute filesystem path — final landing
hash: "sha3-512", // "sha3-512" | "sha-256" | "sha-512" | "shake256"
expected: undefined, // hex digest; when set, verified before rename
timeoutMs: undefined, // wall-clock cap
maxBytes: undefined, // positive integer — abort past this size
audit: undefined, // audit sink with safeEmit({...})
}
Streams a remote resource to disk while hashing the bytes in flight, then atomically renames the tmp file to opts.dest only after the hash matches opts.expected (when supplied). Hash mismatch deletes the tmp file and throws httpclient/hash-mismatch. Composes through request({ responseMode: "stream" }) so the SSRF gate, allowedHosts filter, network proxy, and per-origin transport cache all apply.
var result = await b.httpClient.downloadStream({
url: "https://example.com/release.tar.gz",
dest: "/var/lib/blamejs/release.tar.gz",
hash: "sha3-512",
expected: "9f86d081884c7d65...d4e5",
});
// → { statusCode: 200, bytesWritten: 1048576, hash: "9f86d081884c7d65...d4e5" }
b.httpClient.uploadMultipartStream(opts) #
{
url: , // string — destination
file: , // { path, fieldName, filename?, contentType? }
fields: undefined, // object — extra form fields { name: value, ... }
timeoutMs: undefined, // wall-clock cap
maxBytes: undefined, // positive integer — refuse files larger than this
audit: undefined, // audit sink with safeEmit({...})
}
POSTs a file body via multipart/form-data without buffering the file in memory. Streams from disk through the request body using fs.createReadStream + node:stream/promises pipeline. Throws httpclient/missing-file when opts.file.path doesn't exist or isn't a regular file. Composes through request() so SSRF gating, proxy routing, and the per-origin transport cache apply unchanged.
var res = await b.httpClient.uploadMultipartStream({
url: "https://example.com/upload",
file: {
path: "/var/lib/blamejs/release.tar.gz",
fieldName: "artifact",
contentType: "application/gzip",
},
fields: { releaseTag: "v1.2.3" },
});
// → { statusCode: 200, headers: { ... }, body: }
Last updated 2026-08-08T16:39:49.652Z by seeder.