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

@zelthr/request

v1.7.0

Published

Simplified HTTP request client. Modern remake of the classic request package.

Readme

@zelthr/request

Simplified HTTP request client for Node.js — a modern remake of the classic request package, with HTTP/2, keep-alive pooling and modern streaming built in while keeping the classic request API.

  • Same familiar API: request(url, callback), convenience verbs, defaults, jar, forever, streaming.
  • Native promises: the Request returned by any call is a thenable, so await request(url) just works — no wrapper library needed. An explicit request.promise() helper is included too.
  • HTTP/1.1 and HTTP/2, redirects, cookies, gzip, basic/bearer/digest auth, multipart/form-data and application/x-www-form-urlencoded bodies, HTTP proxies and HTTPS CONNECT tunneling — all out of the box.
  • Got-style extras: hooks, built-in retry with backoff, AbortController cancellation, configurable connection pooling, modern streaming and a paginate() async generator.
  • TypeScript types included. Plain JavaScript (CommonJS + ESM), zero build step, zero runtime dependencies — built directly on Node's http/https/http2. Requires Node.js >= 18.

Attribution: This project is a fork/remake of request by Mikeal Rogers, licensed under the Apache License 2.0. Modified by zelthrStudio (2026). See NOTICE for the attribution notices.

Install

npm install @zelthr/request

Super simple to use

const request = require('@zelthr/request')

request('http://www.google.com', function (error, response, body) {
  console.error('error:', error)
  console.log('statusCode:', response && response.statusCode)
  console.log('body:', body)
})

Table of contents

Request options

Options are passed either as an object or as a URL string:

request({
  uri: 'http://api.github.com/user',
  method: 'GET',
  headers: { 'User-Agent': 'zelthr/request' }
}, callback)

| Option | Description | | --- | --- | | uri / url | Fully qualified URI or parsed URL object. | | method | HTTP method, default GET. | | qs | Object of query-string values added to the URI. | | headers | HTTP request headers. | | body | Request body: string, Buffer, array, Node Readable, web ReadableStream, or async iterable. | | json | true to send/parse JSON, or an object/string to serialize. An invalid JSON response rejects with code: 'EJSONPARSE'. | | form | Object/string encoded as application/x-www-form-urlencoded. | | multipart | Array of multipart parts (see Forms). | | auth | { user, pass, sendImmediately, bearer } for basic/bearer auth. | | followRedirect | Follow redirects, default true. | | followAllRedirects | Follow non-GET redirects too, default false. | | maxRedirects | Maximum number of redirects, default 10. | | gzip | true to request and transparently decode gzip/deflate responses. | | brotli | true (with gzip) to also advertise and decode Brotli (br) responses. | | cache | RFC 7234 HTTP cache: true (shared store), an HttpCache instance, or { ttl, maxEntries }. See Caching. | | dnsCache | DNS result cache: true (shared), { ttl, max }, or a custom lookup function. | | lookup | Custom DNS lookup function passed to the transport. | | progress | true to emit progress events while uploading/downloading. | | mock | Mock this request: a { statusCode, headers, body } spec or a function returning one (or null to pass through). The body must be a string, Buffer, or stream. | | timeout | Timeout in milliseconds (headers + body idle). | | jar | Cookie jar (from request.jar()) to persist cookies across requests. | | proxy | Proxy URL; also read from HTTP_PROXY/HTTPS_PROXY/NO_PROXY env vars. | | tunnel | Accepted for compatibility (tunneling is automatic). | | pool | false to open a new connection per request. | | agent | Custom http.Agent/https.Agent (or any Node agent) for connection pooling. | | forever | Use keep-alive agents (true or {} for agent options). | | time | true to record timings on the response. | | retry | true, a number, or a retry config to retry failed requests. | | dedupe | true to coalesce concurrent identical GET/HEAD requests onto one network request. See Deduplication. | | schema | Validate the parsed response body (joi/zod/valibot-style validator or a plain function). See Schema validation. | | circuitBreaker | true, a threshold number, or { threshold, cooldown } — fail fast per host:port after repeated failures. | | rateLimit | true, req/sec number, or { rate, capacity } — per-host:port token bucket. | | hooks | { beforeRequest, afterResponse } hooks. | | paginate | { transform, filter, ... } options for paginate(). | | rejectUnauthorized | TLS: verify the server certificate, default true. | | ca | TLS: override the trusted CA certificates. | | checkServerIdentity | TLS: custom hostname verification function. | | http2 | true to use HTTP/2 (h2 for https:, h2c for http:). | | localAddress | Bind to a specific local interface. | | strictSSL | Alias of rejectUnauthorized. | | encoding | Response body encoding ('utf8', null for a Buffer, ...). | | qsStringifyOptions / qsParseOptions | Options forwarded to the built-in query-string encoder. |

Convenience methods

request.get(url, callback)
request.post(url, callback)
request.put(url, callback)
request.patch(url, callback)
request.head(url, callback)
request.del(url, callback)   // also request.delete
request.options(url, callback)

request.defaults(options)

Returns a wrapper with default options applied to every request:

const client = request.defaults({
  baseUrl: 'https://api.example.com',
  headers: { Authorization: 'Bearer token' }
})

client('/users', callback)

Caching

The cache option enables an RFC 7234 HTTP cache (in-process, in-memory). GET responses are stored when cacheable and served without touching the network while fresh; stale entries are revalidated with If-None-Match / If-Modified-Since and refreshed on a 304:

// Shared store (usable across requests, even without `cache: true`):
const response = await request.promise({ uri: url, cache: true })
response.fromCache    // true when served from the cache
response.revalidated  // true when refreshed by a 304

request.cache.clear() // empty the shared store

cache: true uses the shared store exposed as request.cache; pass { ttl, maxEntries } or your own HttpCache instance for a dedicated store. Vary headers are honored, and responses with no-store, Authorization requests without public, or non-GET methods are never stored.

Mocking

The mocking layer intercepts requests before they hit the network:

request.mock.add('/users', (uri, req) => ({
  statusCode: 200,
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify([{ id: 1 }])
}))

const response = await request.promise('http://api.example.com/users')
response.isMock // true

request.mock.clear()   // remove all mocks
request.mock.disable() // bypass mocks until enable()

Matchers can be a URL substring, a RegExp, or a (uri, request) => boolean function. Handlers may be async and return null to pass through to the network. A per-request mock option does the same for a single request.

request.mock.enable() warns when NODE_ENV is not test: mocks left on outside a test environment silently serve fake responses to real traffic.

Promises

Every request(...) call returns a Request stream that is also a thenable, so it can be awaited natively — no wrapper package or extra API required:

async function main () {
  const response = await request('http://www.google.com')
  console.log('statusCode:', response.statusCode)
  console.log('body:', response.body)

  const json = await request.post({
    uri: 'https://example.com/api',
    json: { hello: 'world' }
  })
  console.log(json.body) // response.body is already parsed with json: true
}

main().catch(console.error)

The promise resolves with the full response object (with .body populated) once the response is complete, and rejects on request errors and aborts:

const response = await request(url)
  .then((res) => res.body)      // chain like any promise
  .catch((err) => {             // network errors, timeouts, aborts
    console.error(err.code)
    return null
  })
  .finally(() => console.log('done'))

An explicit helper reads even better in TypeScript:

const response = await request.promise(url, { json: true })

The callback, event and streaming APIs are unchanged and can be mixed freely: request(url).pipe(fs.createWriteStream(...)) and await request(url) work on the same request object.

Hooks

Got-style lifecycle hooks:

  • beforeRequest — called with the Request before each attempt (including every retry). May mutate the request; throwing aborts the request.
  • afterResponse — called with the final response. May return a replacement response ({ statusCode, headers, body }) or throw. Redirect responses are skipped.
request({
  uri: 'https://api.example.com/data',
  hooks: {
    beforeRequest: [
      function (req) {
        req.setHeader('x-trace', crypto.randomUUID())
      }
    ],
    afterResponse: [
      function (response) {
        if (response.statusCode === 404) {
          return { statusCode: 404, body: '{}' } // normalize missing data
        }
        return undefined // keep the original response
      }
    ]
  }
}, callback)

Each option accepts a single function or an array of functions; async functions are awaited in order.

Retry

Retries are opt-in (retry: false by default) and only happen when the request body is replayable (string/Buffer/array — or no body at all), so streamed uploads are never re-sent.

request({
  uri: 'https://api.example.com/items',
  retry: true,                       // sensible defaults
  // retry: 3                        // just the limit
  // retry: {                        // full control
  //   limit: 5,
  //   statusCodes: [429, 503],
  //   errorCodes: ['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT'],
  //   maxRetryAfter: 30000,         // cap for Retry-After
  //   backoff: 1000,                // base ms, exponential (or a function)
  //   jitter: true
  // }
}, callback)

Defaults: limit: 3, methods GET/HEAD/OPTIONS, status codes 429 and 503, network error codes, exponential backoff from 1000 ms. A Retry-After response header is honored (capped by maxRetryAfter).

PUT/DELETE are not retried by default: retrying a mutation whose first attempt actually reached the server (but whose response was lost) would duplicate the side effect. If the endpoint is idempotent (for example it accepts an idempotency key), opt back in with retry: { methods: ['PUT'] }.

Reliability: dedupe, schema validation, circuit breaker, rate limit

Four opt-in features for keeping concurrent workloads under control. All of them are per-request options and all are implemented in-process, with no extra dependencies.

Deduplication

dedupe: true coalesces concurrent identical GET/HEAD requests onto a single network request; every waiter receives its own copy of the buffered response (response.fromDedupe === true on the waiters). This is the SWR/React Query pattern for server-side callers. Only idempotent methods are ever coalesced, and the primary's timeout governs the shared attempt. Two requests with different Authorization or Cookie headers are never coalesced — credentials are part of the coalescing key, so one caller's response can never be replayed to another.

const [a, b] = await Promise.all([
  request.promise({ uri: 'https://api.example.com/status', dedupe: true }),
  request.promise({ uri: 'https://api.example.com/status', dedupe: true })
]) // one network request

Schema validation

schema validates the parsed response body before it is delivered. Duck-typing supports the popular validators without bundling them: a joi object (.validate), a zod schema (.parse, errors propagate as ZodError), a valibot schema (.safeParse), or a plain function. The validated (possibly transformed) value replaces response.body; a failed validation rejects the request.

const zodSchema = z.object({ ok: z.boolean() })
request.promise({ uri: 'https://api.example.com/check', json: true, schema: zodSchema })

Circuit breaker

circuitBreaker fails fast when a host keeps failing, instead of hammering a dying endpoint with retries. State is keyed per host:port — different ports never share a circuit. After threshold consecutive final failures the circuit opens; every request then errors with code CB_OPEN until the cooldown elapses, at which point a single half-open probe is allowed through. A successful probe closes the circuit.

request({
  uri: 'https://api.example.com/',
  circuitBreaker: { threshold: 5, cooldown: 30000 }
}, callback)

Rate limiting

rateLimit throttles requests per host:port with a token bucket: capacity tokens may be spent at once (burst), then tokens refill at rate per second. Waiters queue in-process until a token is free; an abort while waiting rejects with AbortError.

request({
  uri: 'https://api.example.com/search?q=item',
  rateLimit: { rate: 5, capacity: 10 } // 5 req/s, burst of 10
}, callback)

rateLimit: true defaults to 10 req/s (burst 10); a number sets both. Non-positive rates and capacities are configuration errors and are rejected loudly at construction (a token bucket that never refills would hang every request); rateLimit: 0 disables the limiter.

Pagination

request.paginate(uri, options) is an async generator that follows pages until they run out. By default the next URL comes from a Link: rel="next" header, falling back to a next field on a JSON body.

for await (const item of request.paginate('https://api.example.com/users', {
  json: true,
  paginate: {
    transform: (response) => response.body.data,
    filter: (item) => item.active,
    countLimit: 100,
    requestLimit: 20,
    backoff: 200
  }
})) {
  console.log(item.name)
}

Pagination options: transform (map a response to items), filter, shouldContinue (stop early), nextUrl (custom page resolver), countLimit, requestLimit and backoff (delay between pages).

Cookies

const jar = request.jar()
request({ uri: 'https://example.com/login', jar }, function (err, res) {
  request({ uri: 'https://example.com/account', jar }, function (err, res, body) {
    console.log(body)
  })
})

A global request.cookie(str) helper parses cookie strings, and request.jar() creates an isolated jar. Any CookieJar-compatible object (a setCookie/getCookieString pair) can be passed as jar.

Streaming

Any request is a readable stream of the response body:

request('https://example.com/big-file.mp4').pipe(fs.createWriteStream('out.mp4'))

And any writable stream can be used as the request body:

fs.createReadStream('in.txt').pipe(request.post('https://example.com/upload'))

Web streams and async iterables work as body too:

const response = await request.promise({
  uri: 'https://example.com/upload',
  method: 'POST',
  body: new Blob(['hello']).stream()
})

request(...).pipe(dest) also copies the response headers onto dest when it is a http.ServerResponse, so it can act as a pass-through proxy.

HTTP/2

Pass http2: true to send the request over HTTP/2:

const response = await request({
  uri: 'https://example.com/api',
  http2: true,
  json: true
})
console.log(response.body)
  • https: URLs negotiate HTTP/2 via ALPN (h2); http: URLs use cleartext HTTP/2 (h2c, prior knowledge).
  • Connections are pooled and multiplexed — no Agent or forever options needed.
  • TLS options (ca, rejectUnauthorized, cert, key, ...) work as usual, and redirects, gzip, cookies, JSON bodies, forms and promises all behave the same as over HTTP/1.
  • Not supported together with proxy (an error is emitted if both are set).
  • http2ConnectTimeout bounds the connection phase (TCP+TLS+ALPN) in milliseconds. Defaults to the request timeout, or 30 s for slow handshakes; the request times out with ETIMEDOUT if the session is not established within the budget.

HTTP/1.1 remains the default; http2 must be enabled per request (or baked into a request.defaults({ http2: true }) wrapper).

Forms

application/x-www-form-urlencoded:

request.post('https://example.com/login', { form: { user: 'bob', pass: 'secret' } }, callback)

multipart/form-data:

request.post('https://example.com/upload', {
  multipart: [
    { 'content-type': 'application/json', body: JSON.stringify({ hello: 'world' }) },
    { 'content-type': 'text/plain', body: 'plain text', 'content-length': 10 }
  ]
}, callback)

Or use request.post(...).form() for a form-data-style instance (with the append/getHeaders/getLength/pipe API, built in — no dependency):

const form = request.post('https://example.com/upload').form()
form.append('file', fs.createReadStream('photo.png'))

JSON

request.post('https://example.com/api', { json: { hello: 'world' } }, function (err, res, body) {
  console.log(body.hello) // body is already parsed
})

Authentication

Basic and bearer auth are sent immediately by default:

request.get({ uri: 'https://example.com/private', auth: { user: 'bob', pass: 'secret' } }, callback)
request.get({ uri: 'https://example.com/private', auth: { bearer: 'token' } }, callback)

Basic auth in the URL is supported too (https://user:[email protected]/).

Digest auth is used when the server responds with a 401 and a WWW-Authenticate: Digest challenge.

Proxies

request({ uri: 'http://example.com/', proxy: 'http://user:[email protected]:8080' }, callback)

When no proxy option is given, the HTTP_PROXY, HTTPS_PROXY and NO_PROXY environment variables are consulted. HTTPS requests tunnel through the proxy with a CONNECT request automatically; HTTP requests are sent in absolute-form. Proxy credentials come from the proxy URL (http://user:pass@proxy:8080).

Timeouts and errors

request({ uri: 'http://example.com/', timeout: 5000 }, function (err, res, body) {
  if (err) {
    console.error(err.code) // 'ETIMEDOUT', 'ECONNREFUSED', ...
  }
})

A timeout before the response arrives surfaces as ETIMEDOUT with err.connect === true; a timeout while the response is being read maps to ESOCKETTIMEDOUT. Errors are delivered to the callback and emitted on the request object.

TLS

request({
  uri: 'https://self-signed.local/',
  ca: fs.readFileSync('ca.crt'),
  checkServerIdentity: function () { return undefined }
}, callback)

rejectUnauthorized: false disables certificate verification. Custom ca, cert, key, pfx, passphrase, ciphers and secureProtocol options are supported.

Connection pooling

Requests share a keep-alive pool by default:

  • pool: false — a fresh connection per request (closed when done).
  • agent — plug in a custom http.Agent/https.Agent to take full control over pooling and connection limits.
  • Requests with custom TLS settings automatically use a pool keyed by those settings, so ca/rejectUnauthorized/client certificates don't leak between requests.
  • request.closePool() closes all pooled connections — handy to let a process exit promptly in tests or long-running CLI tools.
const client = request.defaults({ agent: new http.Agent({ keepAlive: true }) })
const response = await client.promise('https://example.com/')

Web & Edge runtimes (Next.js)

The main package is Node-only (it speaks directly to http/net/tls). For runtimes without the Node http stack — Next.js middleware & Edge runtime, Vercel Edge Functions, Cloudflare Workers, Deno and browsers — use the fetch-based entry:

import request from '@zelthr/request/web'   // alias: '@zelthr/request/edge'

The API mirrors the main package: callback style, request.promise(), convenience verbs (get/post/...), request.defaults(), plus the same opt-in reliability features (dedupe, schema, circuitBreaker, rateLimit — implemented in-process, no dependencies). It has no Node built-in imports, so it bundles cleanly for any web target.

// Next.js Route Handler / Server Component (Edge runtime)
import request from '@zelthr/request/web'

export async function GET () {
  const response = await request.promise({
    uri: 'https://api.example.com/status',
    json: true,
    schema: { parse: (body) => { if (!body.ok) throw new Error('bad') ; return body } },
    dedupe: true,                        // coalesce concurrent identical calls
    circuitBreaker: { threshold: 5, cooldown: 30000 },
    rateLimit: { rate: 10, capacity: 10 }
  })
  return Response.json(response.body)
}

Differences from the main package (all documented limitations):

  • Buffered only — responses are collected into response.body (a string, or a Uint8Array with encoding: null); there are no Node streams. response.headers is a plain object.
  • time: true records elapsedTime and timings.total only.
  • timeout is a total per-attempt budget (not per-phase); it rejects with code: 'ETIMEDOUT'.
  • Network failures reject with code: 'ENETWORK' (the underlying fetch error is attached as cause).
  • gzip is a no-op — fetch advertises accept-encoding and decompresses transparently (gzip and br are both decoded by the platform).
  • Options this entry cannot honor are rejected with code: 'EUNSUPPORTED' instead of being silently ignored (so a caller migrating from the Node client never gets different behavior without noticing): retry, jar/cookies, proxy, cache, mock, paginate, streaming, http2/TLS options, forever/pool/agent, progress, DNS options, brotli (fetch always advertises and decodes br, so brotli: false cannot be honored), removeRefererHeader, jsonReplacer, useQuerystring/qsParseOptions, and plain-object formData (pass a FormData instance instead). Cookies are handled by the platform (undici/Cloudflare), and proxies are typically rewrites in middleware.

Framework integrations

@zelthr/request is a plain Node package, so it drops into any framework's server-side code with no adapter:

// Next.js Route Handler (Node runtime) — app/api/items/route.js
import { NextResponse } from 'next/server'
import request from '@zelthr/request'

export async function GET () {
  const response = await request.promise({
    uri: 'https://api.example.com/items',
    json: true,
    qs: { limit: 20 }
  })
  return NextResponse.json(response.body)
}
// Express / Fastify / NestJS — anything with a Node handler
const request = require('@zelthr/request')

app.get('/weather', async (req, res) => {
  const response = await request.promise({
    uri: 'https://api.example.com/weather',
    qs: req.query,
    json: true,
    time: true
  })
  res.json({ body: response.body, timings: response.timings })
})

Timings

request({ uri: 'http://example.com/', time: true }, function (err, response) {
  console.log(response.timings)       // { wait, dns, tcp, firstByte, download, total }
  console.log(response.timingStart)
})

TypeScript

Types are bundled ("types" field) for both CommonJS and ESM consumers — no @types package needed:

// CommonJS
import request = require('@zelthr/request')

// ESM / NodeNext
import request from '@zelthr/request'
import { get, promise, paginate } from '@zelthr/request'
import type { CoreOptions, Response, RetryOptions } from '@zelthr/request'

const response = await promise('https://example.com/api', { json: true })
const status: number = response.statusCode

const client = request.defaults({ baseUrl: 'https://api.example.com' })
for await (const item of client.paginate('/users', { json: true })) {
  // item: any
}

ESM / CommonJS

The package ships dual: require('@zelthr/request') and import request from '@zelthr/request' both work, with named exports (get, post, del, promise, paginate, ...) available to ESM importers.

API reference

  • request(options, [callback]) — returns the Request stream, which is thenable and can be awaited (see Promises).
  • request(uri, [callback]) — shorthand.
  • request.defaults(options) — preconfigured wrapper.
  • request.get/post/put/patch/head/del/delete/options(...) — verb helpers.
  • request.promise(uri, options) — returns a Promise<Response>.
  • request.paginate(uri, options) — async generator over paginated results.
  • request.jar([store]) — a cookie jar.
  • request.cookie(str) — parse a cookie string.
  • request.forever([agentOptions]) — wrapper using keep-alive agents.
  • request.closePool() — close all pooled connections.
  • request.cache — the shared RFC 7234 HTTP cache (clear(), size).
  • request.mock — the global mocking layer (add, clear, enable, disable).

Running the tests

npm test          # node --test "tests/test-*.js" "tests/test-*.mjs"
npm run lint      # standard
npm run typecheck # tsc --noEmit

License

Apache-2.0