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

node-wreq

v2.3.0

Published

HTTP client with native TLS, HTTP2, JA3, JA4 browser impersonation backed by wreq's Rust core

Downloads

2,690

Readme

node-wreq

NPM Version ESM CJS Platforms

node-wreq is a thin Node.js wrapper around 0x676e67's wreq — a Rust HTTP client exposing its full power to JavaScript.

Use it when you need low-level control over the network layer: TLS configuration, transport fingerprinting, browser impersonation, or fine-grained HTTP/WebSocket behavior that standard Node.js clients simply don't expose.

[!TIP]

why does this exist?

Node.js ships with a built-in https module, and the ecosystem offers popular clients like axios, got, and node-fetch — but all of them are built on top of OpenSSL via Node's tls module, which exposes no control over low-level TLS handshake parameters. This makes it fundamentally impossible to emulate real browser network behavior from pure JavaScript.

  • HTTP/1 over TLS

    Node.js HTTP clients normalize headers to lowercase internally, which is compliant with HTTP/2 semantics but breaks compatibility with some WAFs that enforce case-sensitive header validation on HTTP/1 requests. This wrapper preserves header case exactly as specified, preventing requests from being silently rejected.

  • HTTP/2 over TLS

    Fingerprints like JA3, JA4, and Akamai HTTP/2 are derived from the specifics of the TLS handshake and HTTP/2 SETTINGS frames — cipher suite ordering, TLS extensions, ALPN values, HPACK header compression parameters, and more. Node.js exposes none of these through its tls or http2 APIs. You simply cannot spoof them from JS land, no matter the library. This package solves that at the native layer, giving you fine-grained control over TLS and HTTP/2 extensions to precisely match real browser behavior.

  • Device Emulation

    Because TLS and HTTP/2 fingerprints evolve slowly relative to browser release cycles, a single fingerprint profile often covers many browser versions. 100+ pre-built browser device profiles are bundled, so you don't have to figure out the right combination of settings yourself.

TLS and HTTP/2 fingerprinting is actively used by major bot protection and WAF providers — including Cloudflare Bot Management, AWS WAF (Bot Control + CloudFront JA3 headers), Google Cloud Armor, Akamai (which maintains its own HTTP/2 fingerprint format on top of JA3/JA4), ServicePipe (a Russian DDoS protection and WAF provider), and various specialized anti-bot services like DataDome and PerimeterX. Correctly emulating a browser's TLS handshake and HTTP/2 SETTINGS frames is a hard requirement to get past these layers undetected.

TLS Fingerprinting with JA3 and JA3S

JA3/JA4 Fingerprint — Cloudflare Bot Solutions

TLS Fingerprinting: How It Works & How to Bypass It

[!NOTE] This only covers the transport layer. It does not help bypass JavaScript-based challenges (Cloudflare Turnstile, Akamai sensor data, Kasada, etc.), CAPTCHA, or behavioral analysis — those require a different approach entirely

install

npm install node-wreq

Node.js 20+ is required.

contents

⚡   quick start

🌐   fetch

🧩   client — shared defaults, reusable config.

🎭   browser profiles

🪝   hooks — request lifecycle, dynamic auth, retries, etc.

🍪   cookies and sessions

🔁   redirects and retries

📊   observability

🚨   error handling

🔌   websockets

🧪   networking / transport knobs — TLS, HTTP/1, HTTP/2 options; header ordering, mTLS and custom CAs; DNS controls.

quick start

import { fetch } from 'node-wreq';

const response = await fetch('https://httpbin.org/get', {
  browser: 'chrome_137',
});

console.log(response.status);
console.log(await response.json());

If you keep repeating config, move to a client:

import { createClient } from 'node-wreq';

const client = createClient({
  baseURL: 'https://httpbin.org',
  browser: 'chrome_137',
  headers: {
    'x-client': 'node-wreq',
  },
  retry: 2,
});

const response = await client.fetch('/anything', {
  query: { from: 'client' },
});

console.log(response.status);
console.log(await response.json());

🌐 fetch   ·   

simple GET

import { fetch } from 'node-wreq';

const response = await fetch('https://httpbin.org/get', {
  browser: 'firefox_139',
  query: {
    source: 'node-wreq',
    debug: true,
  },
  timeout: 15_000,
});

const body = await response.json();

console.log(response.ok);
console.log(body.args);

JSON POST

import { fetch } from 'node-wreq';

const response = await fetch('https://api.example.com/items', {
  method: 'POST',
  browser: 'chrome_137',
  headers: {
    'content-type': 'application/json',
  },
  body: JSON.stringify({
    name: 'example',
    enabled: true,
  }),
  throwHttpErrors: true,
});

console.log(await response.json());

upload FormData

FormData request bodies work like fetch: the multipart boundary and content-type header are generated automatically.

const formData = new FormData();

formData.append('alpha', '1');
formData.append('upload', new File(['hello'], 'hello.txt', { type: 'text/plain' }));

const response = await fetch('https://api.example.com/upload', {
  method: 'POST',
  body: formData,
});

console.log(await response.json());

build a Request first

import { Request, fetch } from 'node-wreq';

const request = new Request('https://httpbin.org/post', {
  method: 'POST',
  headers: {
    'content-type': 'application/json',
  },
  body: JSON.stringify({ via: 'Request' }),
});

const response = await fetch(request, {
  browser: 'chrome_137',
});

console.log(await response.json());

read extra metadata

fetch() returns a fetch-style Response, plus extra metadata under response.wreq.

const response = await fetch('https://example.com', {
  browser: 'chrome_137',
});

console.log(response.status);
console.log(response.headers.get('content-type'));

console.log(response.wreq.cookies);
console.log(response.wreq.setCookies);
console.log(response.wreq.timings);
console.log(response.wreq.redirectChain);

If you need a Node stream instead of a WHATWG stream:

const readable = response.wreq.readable();

readable.pipe(process.stdout);

🧩 client   ·   

Use createClient(...) when requests share defaults:

  • baseURL
  • browser profile
  • headers
  • proxy
  • timeout
  • hooks
  • retry policy
  • cookie jar

shared defaults

import { createClient } from 'node-wreq';

const client = createClient({
  baseURL: 'https://api.example.com',
  browser: 'chrome_137',
  timeout: 10_000,
  headers: {
    authorization: `Bearer ${process.env.API_TOKEN}`,
  },
  retry: {
    limit: 2,
    statusCodes: [429, 503],
  },
});

const users = await client.get('/users');

console.log(await users.json());

const created = await client.post(
  '/users',
  JSON.stringify({ email: '[email protected]' }),
  {
    headers: {
      'content-type': 'application/json',
    },
  }
);

console.log(created.status);

extend a client

const base = createClient({
  baseURL: 'https://api.example.com',
  browser: 'chrome_137',
});

const admin = base.extend({
  headers: {
    authorization: `Bearer ${process.env.ADMIN_TOKEN}`,
  },
});

await base.get('/health');
await admin.get('/admin/stats');

🎭 browser profiles   ·   

Inspect the available profiles at runtime:

import { getProfiles } from 'node-wreq';

console.log(getProfiles());

There is also BROWSER_PROFILES if you want the generated list directly.

Typical profiles include browser families like:

  • Chrome
  • Edge
  • Firefox
  • Safari
  • Opera
  • OkHttp

🪝 hooks   ·   

Hooks are the request pipeline.

Available phases:

  • init
  • beforeRequest
  • afterResponse
  • beforeRetry
  • beforeError
  • beforeRedirect

common pattern: auth, tracing, proxy rotation

import { createClient } from 'node-wreq';

const client = createClient({
  baseURL: 'https://example.com',
  retry: {
    limit: 2,
    statusCodes: [429, 503],
    backoff: ({ attempt }) => attempt * 250,
  },
  hooks: {
    init: [
      ({ options, state }) => {
        options.query = { ...options.query, source: 'hook-init' };

        state.startedAt = Date.now();
      },
    ],
    beforeRequest: [
      ({ request, options, state }) => {
        request.headers.set('x-trace-id', crypto.randomUUID());
        request.headers.set('authorization', `Bearer ${getAccessToken()}`);

        options.proxy = pickProxy();

        state.lastProxy = options.proxy;
      },
    ],
    beforeRetry: [
      ({ options, attempt, error, state }) => {
        options.proxy = pickProxy(attempt);

        console.log('retrying', {
          attempt,
          proxy: options.proxy,
          previousProxy: state.lastProxy,
          error,
        });
      },
    ],
    beforeError: [
      ({ error, state }) => {
        error.message = `[trace=${String(state.startedAt)}] ${error.message}`;

        return error;
      },
    ],
  },
});

replace a response in afterResponse

import { Response, fetch } from 'node-wreq';

const response = await fetch('https://example.com/account', {
  hooks: {
    afterResponse: [
      async ({ response }) => {
        if (response.status === 401) {
          return new Response(JSON.stringify({ guest: true }), {
            status: 200,
            headers: {
              'content-type': 'application/json',
            },
            url: response.url,
          });
        }
      },
    ],
  },
});

console.log(await response.json());

mutate redirect hops

await fetch('https://example.com/login', {
  hooks: {
    beforeRedirect: [
      ({ request, nextUrl, redirectCount }) => {
        request.headers.set('x-redirect-hop', String(redirectCount));
        request.headers.set('x-next-url', nextUrl);
      },
    ],
  },
});

Rule of thumb:

  • use hooks for dynamic behavior
  • use client defaults for static behavior

🍪 cookies and sessions   ·   

node-wreq does not force a built-in cookie store.

You provide a cookieJar with two methods:

  • getCookies(url)
  • setCookie(cookie, url)

That jar can be:

  • in-memory
  • tough-cookie
  • Redis-backed
  • DB-backed
  • anything else that matches the interface

tiny in-memory jar

import { fetch, websocket } from 'node-wreq';

const jarStore = new Map<string, string>();

const cookieJar = {
  getCookies() {
    return [...jarStore.entries()].map(([name, value]) => ({
      name,
      value,
    }));
  },
  setCookie(cookie: string) {
    const [pair] = cookie.split(';');
    const [name, value = ''] = pair.split('=');

    jarStore.set(name, value);
  },
};

await fetch('https://example.com/login', { cookieJar });
await fetch('https://example.com/profile', { cookieJar });
await websocket('wss://example.com/ws', { cookieJar });

tough-cookie

npm install tough-cookie
import { CookieJar as ToughCookieJar } from 'tough-cookie';
import { createClient } from 'node-wreq';

const toughJar = new ToughCookieJar();

const cookieJar = {
  async getCookies(url: string) {
    const cookies = await toughJar.getCookies(url);

    return cookies.map((cookie) => ({
      name: cookie.key,
      value: cookie.value,
    }));
  },
  async setCookie(cookie: string, url: string) {
    await toughJar.setCookie(cookie, url);
  },
};

const client = createClient({
  browser: 'chrome_137',
  cookieJar,
});

await client.fetch('https://example.com/login');
await client.fetch('https://example.com/profile');

inspect cookies on a response

import { fetch } from 'node-wreq';

const response = await fetch('https://example.com/login', { cookieJar });

console.log(response.wreq.setCookies);
console.log(response.wreq.cookies);

🔁 redirects and retries   ·   

Both are opt-in controls on top of the normal request pipeline.

manual redirects

const response = await fetch('https://example.com/login', {
  redirect: 'manual',
});

console.log(response.status);
console.log(response.headers.get('location'));
console.log(response.redirected);

Modes:

  • follow - default redirect following
  • manual - return the redirect response as-is
  • error - throw on the first redirect

Useful redirect facts:

  • response.wreq.redirectChain records followed hops
  • 301 / 302 rewrite POST to GET
  • 303 rewrites to GET unless current method is HEAD
  • 307 / 308 preserve method and body
  • authorization is stripped on cross-origin redirect

simple retries

const response = await fetch('https://example.com', {
  retry: 2,
});

explicit retry policy

const response = await fetch('https://example.com', {
  retry: {
    limit: 3,
    statusCodes: [429, 503],
    backoff: ({ attempt }) => attempt * 500,
  },
});

custom retry decision

import { TimeoutError, fetch } from 'node-wreq';

const response = await fetch('https://example.com', {
  retry: {
    limit: 5,
    shouldRetry: ({ error, response }) => {
      if (response?.status === 429) {
        return true;
      }

      return error instanceof TimeoutError;
    },
  },
});

Defaults:

  • retry is off unless you enable it
  • default retry methods are GET and HEAD
  • default status codes include 408, 425, 429, 500, 502, 503, 504
  • default error codes include ECONNRESET, ECONNREFUSED, ETIMEDOUT, ERR_TIMEOUT

📊 observability   ·   

Two main surfaces:

  • response.wreq.timings
  • onStats(stats)

per-request stats callback

await fetch('https://example.com', {
  onStats: ({ attempt, timings, response, error }) => {
    console.log({
      attempt,
      wait: timings.wait,
      total: timings.total,
      status: response?.status,
      error,
    });
  },
});

read timings from the final response

const response = await fetch('https://example.com', {
  browser: 'chrome_137',
});

console.log(response.wreq.timings);

Current timings are wrapper-level timings that are still useful in practice:

  • request start
  • response available
  • total time when body consumption is known

🚨 error handling   ·   

Main error classes:

  • RequestError
  • HTTPError
  • TimeoutError
  • AbortError
  • WebSocketError

Typical patterns:

import { HTTPError, TimeoutError, fetch } from 'node-wreq';

try {
  await fetch('https://example.com', {
    timeout: 1_000,
    throwHttpErrors: true,
  });
} catch (error) {
  if (error instanceof TimeoutError) {
    console.error('request timed out');
  } else if (error instanceof HTTPError) {
    console.error('bad status', error.statusCode);
  } else {
    console.error(error);
  }
}

🔌 websockets   ·   

You can use either:

  • await websocket(url, init?)
  • new WebSocket(url, init?)

simple helper

import { websocket } from 'node-wreq';

const socket = await websocket('wss://echo.websocket.events', {
  browser: 'chrome_137',
  protocols: ['chat'],
});

socket.addEventListener('message', (event) => {
  console.log('message:', event.data);
});

socket.send('hello');

WHATWG-like constructor

import { WebSocket } from 'node-wreq';

const socket = new WebSocket('wss://example.com/ws', {
  binaryType: 'arraybuffer',
});

await socket.opened;

socket.onmessage = (event) => {
  if (event.data instanceof ArrayBuffer) {
    console.log(new Uint8Array(event.data));
  }
};

socket.send(new Uint8Array([1, 2, 3]));
socket.close(1000, 'done');

websocket from a client

Useful when you want shared defaults like browser, proxy, or cookies:

const client = createClient({
  browser: 'chrome_137',
  cookieJar: yourCookieJar,
});

const socket = await client.websocket('wss://example.com/ws');

Notes:

  • cookies from cookieJar are sent during handshake
  • duplicate subprotocols are rejected

🧪 networking / transport knobs   ·   

This is the "transport nerd" section.

Everything else here is for debugging request shape, fingerprint-sensitive targets, or testing transport hypotheses.

browser profile + proxy + timeout

const response = await fetch('https://httpbin.org/anything', {
  browser: 'chrome_137',
  proxy: 'http://username:[email protected]:8080',
  timeout: 10_000,
});

If you want to bypass env/system proxy detection for a specific request, use proxy: false:

await fetch('https://example.com', {
  proxy: false,
});

disable default browser-like headers

By default, node-wreq may apply profile-appropriate default headers.

disableDefaultHeaders: true disables those browser/profile preset headers only.

That means it turns off headers injected by the selected browser emulation, such as:

  • user-agent
  • accept
  • accept-language
  • sec-ch-ua
  • sec-ch-ua-mobile
  • sec-ch-ua-platform
  • sec-fetch-dest
  • sec-fetch-mode
  • sec-fetch-site
  • priority

The exact set varies by profile.

It does not disable protocol or transport-level headers that may still appear automatically, such as:

  • host
  • accept-encoding when compress is enabled
  • content-length when the request body requires it
  • content-type generated by the runtime for bodies like FormData

It also does not remove headers you set explicitly yourself.

If you want full manual control:

await fetch('https://example.com', {
  disableDefaultHeaders: true,
  headers: {
    accept: '*/*',
    'user-agent': 'custom-client',
  },
});

For example, with browser: 'chrome_137', the default request would normally include Chrome-like sec-ch-*, sec-fetch-*, user-agent, accept, and accept-language headers. With disableDefaultHeaders: true, those browser preset headers are skipped, while transport headers like host and accept-encoding may still be present.

exact header order

Use tuples when header order matters.

Tuple headers also preserve the original header names exactly as you wrote them on the wire:

await fetch('https://example.com', {
  headers: [
    ['x-lower', 'one'],
    ['X-Mixed', 'two'],
  ],
});

For example, this will preserve both the tuple order and the exact x-lower / X-Mixed casing you passed.

lower-level transport tuning

If a browser preset gets you close but not all the way there:

await fetch('https://example.com', {
  browser: 'chrome_137',
  tlsOptions: {
    greaseEnabled: true,
  },
  http1Options: {
    writev: true,
  },
  http2Options: {
    adaptiveWindow: false,
    maxConcurrentStreams: 64,
  },
});

Use these only when:

  • a target is still picky after choosing a browser profile
  • you are comparing transport behavior
  • you want to debug fingerprint mismatches

mTLS and custom CAs

Use tlsIdentity for client certificate authentication and ca for a custom trust store:

import { fetch } from 'node-wreq';
import { readFileSync } from 'node:fs';

await fetch('https://mtls.example.com', {
  tlsIdentity: {
    cert: readFileSync('./client-cert.pem'),
    key: readFileSync('./client-key.pem'),
  },
  ca: {
    cert: readFileSync('./ca.pem'),
    includeDefaultRoots: false,
  },
});

PKCS#12 / PFX identities are also supported:

await fetch('https://mtls.example.com', {
  tlsIdentity: {
    pfx: readFileSync('./client-identity.p12'),
    passphrase: 'secret',
  },
  ca: {
    cert: readFileSync('./ca.pem'),
    includeDefaultRoots: false,
  },
});

For TLS diagnostics, you can ask for peer certificate metadata on the response or write TLS session keys to a file for tools like Wireshark:

const response = await fetch('https://mtls.example.com', {
  tlsDebug: {
    peerCertificates: true,
    keylog: {
      path: '/tmp/node-wreq.keys',
    },
  },
});

console.log(response.wreq.tls?.peerCertificate);
console.log(response.wreq.tls?.peerCertificateChain);

Unsafe TLS overrides are separate and explicit:

await fetch('https://staging.internal.example', {
  tlsDanger: {
    certVerification: false,
    verifyHostname: false,
    sni: false,
  },
});

compression

Compression is enabled by default.

That includes gzip, br, deflate, and zstd response decoding when the server supports them.

Disable it if you need stricter control over response handling:

await fetch('https://example.com/archive', {
  compress: false,
});

DNS controls

Use dns.hosts to pin hostnames to specific IPs, or dns.servers to send lookups through specific nameservers:

await fetch('https://api.internal.test/health', {
  dns: {
    servers: ['1.1.1.1', '8.8.8.8'],
    hosts: {
      'api.internal.test': ['127.0.0.1'],
    },
  },
});