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

@gointuition/http-client

v4.0.2

Published

High-performance HTTP client with native C implementation

Downloads

335

Readme

@gointuition/http-client

High-performance HTTP client with native C implementation using N-API.

Prerequisites

1. Build the C shared library

cd <project_root>
cmake -B build && cmake --build build -j$(nproc)

This produces lib/shared/libhttpclient.{dylib,so,dll}.

2. Install the addon

For development from source (builds the native addon locally via node-gyp):

cd nodejs
npm install        # installs devDependencies
npm run build      # compiles httpaddon.node from source

For consuming the published package, no compilation is needed — the npm tarball ships prebuilt addons under prebuilds/<plat>-x64/ and load-addon.js picks the right one automatically (see "Using the Release Artifacts" below).

Windows (MSVC + MinGW dual-toolchain)

On Windows the project uses a split toolchain:

| Component | Compiler | Output | |-----------|----------|--------| | C library | MinGW-w64 (MSYS2 MINGW64) | libhttpclient.dll | | Node.js addon | MSVC (node-gyp default) | httpaddon.node |

Requirements:

  • MSYS2 with MINGW64 toolchain (pacman -S mingw-w64-x86_64-toolchain mingw-w64-x86_64-tools-git)
  • Visual Studio 2022 with "Desktop development with C++" workload
  • NASM, Go (for BoringSSL)
  • Node.js >= 14, CMake >= 3.29

Step 1 — Build the C library (MSYS2 MINGW64 shell):

cd <project_root>
cmake -B build -G "MinGW Makefiles"
cmake --build build -j4

Step 2 — Build the addon (PowerShell):

cd nodejs
npm run build

build-addon.js automates the entire process:

  1. Generates httpclient.lib (MSVC import library) from libhttpclient.dll using gendef + lib.exe
  2. Runs node-gyp rebuild (MSVC compiles the addon)
  3. Copies runtime DLLs to build/Release/:
    • libhttpclient.dll
    • libwinpthread-1.dll, libgcc_s_seh-1.dll, libstdc++-6.dll (MinGW runtime)

Note: zlib is statically linked into libhttpclient.dll, so no external zlib DLL is needed.

Usage

const httpClient = require('./index.js');

// Initialize
httpClient.init();

// Send a request
const result = httpClient.request({
    "method": "GET",
    "url": "https://tls.peet.ws/api/all",
    "connectTimeoutInMilliseconds": 3000,
    "responseReadingTimeoutInMilliseconds": 30000,
    "decompress": 0,
    "log": 1,
    "headers": {
        "host": "tls.peet.ws",
        "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36",
        "sec-ch-ua": "\"Not:A-Brand\";v=\"99\", \"Google Chrome\";v=\"145\", \"Chromium\";v=\"145\"",
        "sec-ch-ua-mobile": "?0",
        "accept": "*/*",
        "sec-fetch-site": "same-origin",
        "sec-fetch-mode": "cors",
        "sec-fetch-dest": "script",
        "accept-encoding": "gzip, deflate, br, zstd",
        "accept-language": "en-US,en;q=0.9",
        "priority": "u=1"
    },
    "proxy": {
        "scheme": "https",
        "host": "127.0.0.1",
        "port": "24801",
        "authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ="
    },
    "session": {
        "expirationInMilliseconds": 300000
    }
});

console.log(result);

// Cleanup when done
httpClient.cleanup();

Concurrent requests (async)

requestAsync returns a Promise. The native call itself is blocking, so it runs on a libuv worker thread; requests issued together execute in parallel up to the libuv thread-pool size (default 4 — raise it via UV_THREADPOOL_SIZE, see Concurrency & Multiplexing). Same-host requests share one HTTP connection and each takes its own stream, so they are multiplexed rather than serialized.

const httpClient = require('./index.js');
httpClient.init();

const config = {
    method: "GET",
    url: "https://www.cloudflare.com/cdn-cgi/trace",
    headers: { "host": "www.cloudflare.com", "user-agent": "Mozilla/5.0 ... Chrome/145.0.0.0 Safari/537.36" },
};

// 8 concurrent requests over a single multiplexed connection
const responses = await Promise.all(
    Array.from({ length: 8 }, () => httpClient.requestAsync(config))
);
responses.forEach((res) => console.log(JSON.parse(res).session.streamId)); // 1, 3, 5, 7, ...

httpClient.cleanup();

API

httpClient.init()

Initialize the HTTP client environment. Returns this for chaining.

httpClient.request(config)

Send an HTTP request.

Parameters:

  • config (Object | string): Request configuration object or JSON string

Returns:

  • Object: Parsed response data

Throws:

  • Error: If request fails

httpClient.requestAsync(config)

Send an HTTP request asynchronously. The underlying native call is blocking (it does synchronous socket I/O in C), so requestAsync offloads it to a libuv worker thread to keep the JS event loop responsive. Multiple pending requests therefore run in parallel only up to the size of the libuv thread pool — see Concurrency & Multiplexing for how to raise it with UV_THREADPOOL_SIZE. Same-host requests are multiplexed over a single HTTP connection (each on its own stream).

Parameters:

  • config (Object | string): Request configuration object or JSON string
  • callbacks (StreamCallbacks, optional): onHeaders / onData / onComplete — all three together or none, see Streaming response

Returns:

  • Promise<string>: Resolves with the response JSON string

httpClient.cleanup()

Cleanup resources and release memory.

Streaming response

requestAsync(config, callbacks) and requestNonBlocking(config, pollIntervalMs, callbacks) take a callback bundle. With one, the decoded body is handed to onData chunk by chunk instead of being buffered, and the resolved JSON reports "streamed": 1 with an empty payload. Without one, the library collects the body itself and the JSON carries it in response.payload. The bundle is all or nothing: a partial one throws before the request goes out, since a missing onData would leave the body with neither a consumer nor a place in the result.

let received = 0;
await httpClient.requestAsync(config, {
    onHeaders: (headers) => console.log(headers[':status']),
    // return true to tear the response down (RST_STREAM); the JSON then reports 3-0014
    onData: (chunk) => { received += chunk.length; return received >= 64 * 1024; },
    onComplete: (error) => console.log(error ? error.code : 'clean'),
});

| Callback | Signature | Notes | |----------|-----------|-------| | onHeaders | (headers: Record<string, string>) => void | Once per attempt, before the body, :status first | | onData | (chunk: Buffer) => boolean \| void | One decoded chunk (gzip / deflate / Brotli / Zstd already inflated) | | onComplete | (error: { code, message } \| null) => void | Once per attempt, after the last chunk; null when the body ended cleanly |

The C core runs the contract on the connection reader thread, so the addon posts each event to the JS thread through a threadsafe function. A JS callback therefore cannot hand its verdict back synchronously: onData returning true is honoured by the next chunk. Events also travel independently of the returned Promise, so wait for onComplete before judging a transfer.

The synchronous request(config) blocks the event loop for the whole exchange, which leaves no thread to deliver callbacks on — it always answers with a buffered response.

Request Configuration

| Field | Type | Description | |-------|------|-------------| | url | string | Target URL (required) | | method | string | HTTP method: GET, POST | | headers | Record<string, string> | Request headers | | payload | object | Request body (for POST) | | connectTimeoutInMilliseconds | number | TCP + TLS connect timeout | | responseReadingTimeoutInMilliseconds | number | Response reading timeout | | decompress | number | Decompression flags: 0 (none), 1 (gzip), 2 (deflate), 4 (br), 8 (zstd), or combinations (e.g. 15 = all) | | log | number | Enable logging: 0 (off), 1 (on) | | proxy | ProxyConfig | Proxy settings | | session | SessionConfig | Session settings |

ProxyConfig

| Field | Type | Description | |-------|------|-------------| | scheme | string | Proxy scheme (e.g. https) | | host | string | Proxy host | | port | string | Proxy port | | authorization | string? | Proxy auth header (e.g. Basic ...) |

SessionConfig

| Field | Type | Description | |-------|------|-------------| | expirationInMilliseconds | number | Session expiration timeout | | clientHelloId | string? | uTLS-style fingerprint profile to emulate (see below) |

Optional clientHelloId pins the TLS/HTTP wire fingerprint. When omitted, it follows the request's User-Agent, and an unrecognized User-Agent falls back to hellochrome_auto. Supported values: hellochrome_auto, hellochrome_152, hellochrome_150, hellocrios_auto, hellocrios_150 (_auto follows the browser major version declared by the User-Agent when a profile exists for it, else the latest emulated version; _<version> pins a specific one; case-insensitive).

Response Structure

interface HttpResult {
    url: string;
    method: string;
    request: {
        headers: string[];
        payload?: string;
    };
    response: {
        statusCode?: number;
        headers?: string[];
        payload?: string;
        streamed?: number;      // 1 when the body went to the callbacks, 0 when buffered
        contentEncoding?: string;
        payloadEncoding?: string;
        payloadSize?: number;
    };
    error: {
        code?: string;
        message?: string;
    };
    session: {
        creationTime?: number;
        streamId?: number;
        expirationInMilliseconds?: number;
    };
}

Running Tests

# Quick test
node test.js

# Streaming callbacks, abort, incremental gzip decode
node test_streaming.js

# Full example
node example.js

# Or use npm
npm test

How It Works

Node.js (N-API)
  → ./build/Release/httpaddon.node
    → C library (BoringSSL + HTTP)
      → HTTP over TLS to server
  • No FFI overhead — direct native binding via N-API
  • Direct memory access — zero-copy buffer sharing between C and JS
  • Automatic buffer management — V8 garbage collection handles JS-side cleanup
  • TLS 1.3 session resumption — automatic pre_shared_key for subsequent connections to the same host

Concurrency & Multiplexing

requestAsync makes HTTP multiplexing usable from JavaScript:

  • Shared connection — concurrent same-host requests reuse one connection (session creation is serialized in the C core, so a burst of requests does not each open its own connection).
  • One stream per request — each request is assigned an odd, incrementing stream id (1, 3, 5, …) on the shared connection.
  • Per-connection reader thread — a single reader thread owns SSL_read and demultiplexes inbound frames to the waiting request by stream id, keeping the shared HPACK dynamic table in wire-arrival order.
  • Connection keep-alive — the client acknowledges server SETTINGS and PING frames, so long-lived multiplexed connections stay open.

Note: some servers close the connection after a single response (they send GOAWAY with Last-Stream-ID = 1); against those, requests fall back to one connection per request. Servers that keep the connection open (e.g. Cloudflare) show the full 1, 3, 5, 7, … stream sequence on one connection.

Blocking calls & the libuv thread pool

The native request is synchronous/blocking by design. requestAsync hides this by running each call on a libuv worker thread, but libuv's pool has only 4 threads by default. That is the real ceiling on concurrency: issuing 8 requestAsync calls with the default pool runs at most 4 at a time while the rest queue for a free worker.

To get true N-way concurrency, raise the pool with the UV_THREADPOOL_SIZE environment variable (valid range 1–1024):

# run 8 requests genuinely in parallel
UV_THREADPOOL_SIZE=8 node your-app.js

Timing matters: libuv creates the pool lazily on the first async operation and its size is fixed from that moment. Setting it from JavaScript only works if you do so before any async work (including the first requestAsync):

// must be the very first line, before requiring the client or any I/O
process.env.UV_THREADPOOL_SIZE = '8';
const httpClient = require('./index.js');

Setting it as an OS-level environment variable before launching Node is the most reliable approach. Size the pool to your target concurrency (UV_THREADPOOL_SIZE = max in-flight requests); note the pool is shared with Node's own file-system and DNS operations, so leave a little headroom if the app also does heavy disk or dns.lookup work.

Using the Release Artifacts

Each GitHub Release ships a standard npm package http-client-nodejs-<ver>.tgz with prebuilt addons under prebuilds/<plat>-x64/httpaddon.node. Install it directly from the asset (no registry needed):

npm install ./http-client-nodejs-1.0.2.tgz

load-addon.js selects the matching prebuilds/<plat>-x64/httpaddon.node automatically — no compilation. The addon still needs libhttpclient next to it (or on the loader path), so also install the C library from the httpclient-<ver>-all.tar.gz release asset.

The native request is synchronous/blocking; requestAsync runs calls on libuv worker threads (default pool size 4). Raise the pool for real concurrency:

UV_THREADPOOL_SIZE=8 node your-app.js

Building from Source

npm run build

TypeScript Support

Type definitions are included (index.d.ts):

import httpClient, { HttpRequestConfig, HttpResult } from '@gointuition/http-client';

const config: HttpRequestConfig = {
    method: "GET",
    url: "https://example.com",
};

const result: HttpResult = httpClient.request(config);

License

Apache-2.0