@ossiana/node-libcurl
v1.10.2
Published
A Node.js HTTP request library based on libcurl, with browser-grade TLS/HTTP2/HTTP3 fingerprint customization. It patches BoringSSL to customize the TLS client hello (cipher suites, extensions, order) and uses a custom HTTP/2 / HTTP/3 implementation, so t
Readme
node-libcurl
A Node.js HTTP request library based on libcurl, with browser-grade TLS/HTTP2/HTTP3 fingerprint customization. It patches BoringSSL to customize the TLS client hello (cipher suites, extensions, order) and uses a custom HTTP/2 / HTTP/3 implementation, so the traffic can be made to look like real Chrome or Firefox — something the built-in Node.js fetch can't do.
Build Status
| Platform | Support | | :-----------------: | :-----: | | Windows (x64) | Yes | | Ubuntu (x86_64) | Yes | | MacOS (x86_64/arm64) | Yes |
How to Install
npm i -g pnpm
pnpm i @ossiana/node-libcurl
Usage
The package exports four APIs:
| Export | Description |
| :----- | :---------- |
| requests | High-level, axios/requests-style API. Recommended. |
| fetch | fetch-like API with a compatible response interface. |
| LibCurl | Low-level wrapper around the native libcurl binding. |
| LibCurlWebSocket | WebSocket client with fingerprint support. |
import { requests, fetch, LibCurl, LibCurlWebSocket } from "@ossiana/node-libcurl";1. requests (axios-style)
Static requests (one-shot)
import { requests } from "@ossiana/node-libcurl";
const resp = await requests.get("https://httpbin.org/get", {
headers: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36",
},
params: { page: 1, size: 20 }, // appended to query string
});
console.log(resp.status); // 200
console.log(resp.text); // response body as string
console.log(resp.json); // response body parsed as JSONSupported methods: get post put patch delete head options trace.
Session (persistent connection + cookies + retry)
A session reuses one LibCurl instance, so the TCP/TLS connection and cookies are kept between requests — pass it a shared instance to get the same behavior across sessions:
const session = requests.session({
httpVersion: "http2",
redirect: true,
timeout: 15, // seconds
// ja3: "chrome131",
// akamai: "auto",
// proxy: "http://user:[email protected]:8888",
});
const a = await session.get("https://example.com/login");
const b = await session.post("https://example.com/login", {
json: { username: "user", password: "pass" }, // sets Content-Type: application/json
// or use data for form-encoded bodies:
// data: { username: "user", password: "pass" },
});Session-only options:
const session = requests.session({
defaultRequestHeaders: { // merged into every request of this session
"User-Agent": "Mozilla/5.0 ... Chrome/150.0.0.0 Safari/537.36",
},
cookies: { // pre-set cookies for the given uri
value: { name: "a", value: "b" },
uri: "https://example.com",
},
autoSortRequestHeaders: "auto", // reorder headers like Chrome fetch
requestType: "fetch", // "fetch" | "XMLHttpRequest"
});Session methods (in addition to the HTTP verbs):
session.setDefaultRequestHeaders(headers);
session.setCookie("key", "value", ".example.com", "/"); // set cookie manually
session.getCookie("key", ".example.com"); // get one cookie value
session.getCookies(); // "a=b; c=d;"
session.getCookiesMap(); // Map<string, { domain, subDomain, path, secure, timestamp, value }>
session.deleteCookie("key", ".example.com");
session.retry(3); // returns a NEW session that retries up to 3 times
session.setProxy(proxy);
session.setTimeout(connectTime, sendTime); // seconds
session.setRedirect(true);
session.setHttpVersion("http2");
session.setInterface("eth0");
session.setJA3Fingerprint("chrome150");
session.setAkamaiFingerprint("auto");
session.setHttp3Fingerprint("auto");
session.getLastEffectiveUrl();retry accepts a condition callback:
const retrySession = session.retry(3, (resp, error) => {
// return true to stop retrying, false to retry again
if (error) return false;
return resp.status < 500;
});Request options
| Option | Type | Description |
| :----- | :--- | :---------- |
| headers | string \| object \| string[] \| [string, string][] | Request headers. String form is "Key: value\nKey2: value2". |
| params | URLSearchParams \| string \| object | Appended to the URL query string. |
| json | object | Sends the object as JSON body, sets Content-Type: application/json. |
| data | string \| Uint8Array \| URLSearchParams \| object | Sends a body. Objects are form-encoded (a=1&b=2), and Content-Type is set automatically. Cannot be combined with json. |
| timeout | number | Timeout in seconds. |
| redirect | boolean | Follow redirects (default false). |
| proxy | string \| { proxy, username, password } | Proxy, e.g. "http://127.0.0.1:8888", "socks5://user:pass@host:1080", or an account object. For HTTP/3 use a SOCKS5 proxy with UDP relay — see HTTP/3 through a SOCKS5 UDP proxy. |
| connectTo | string \| string[] | Connection replacement (CURLOPT_CONNECT_TO): when the request host is HOST:PORT, actually connect to CONNECT-TO-HOST:CONNECT-TO-PORT instead (DNS resolves the connect-to host, while Host/SNI keep the request host). Format: "HOST:PORT:CONNECT-TO-HOST:CONNECT-TO-PORT", e.g. "foo.abc.com:443:static.abc.com:443". |
| httpVersion | "http1.1" \| "http2" \| "http3" \| "http3_only" | HTTP protocol version. |
| interface | string | Bind to a specific network interface. |
| ja3 | see Fingerprints | TLS (JA3) fingerprint. |
| akamai | see Fingerprints | HTTP/2 Akamai fingerprint. |
| http3Fingerprint | see Fingerprints | HTTP/3 (QUIC) fingerprint. |
| autoSortRequestHeaders | "auto" \| "chrome130" \| "chrome131" \| boolean | Auto-sort request headers like Chrome fetch. |
| tlsVerifySigalgs | string \| (string \| number)[] | Custom TLS signature algorithms. |
| requestType | "fetch" \| "XMLHttpRequest" | HTTP/2 pseudo-header ordering style. |
| headersOrder | string[] | Explicit order of request headers for the next request only. |
| h2config | { weight: number, streamId?: number } | Customize the HTTP/2 stream weight / next stream id. |
| sslCert | { certBlob, privateKeyBlob?, type?, password? } | Client certificate (type: "PEM" \| "DER" \| "P12"). |
| sslVerify | { caPath: string } | Custom CA bundle path. |
HTTP/3 through a SOCKS5 UDP proxy
HTTP/3 runs over QUIC, which uses UDP instead of TCP — so a regular HTTP/HTTPS proxy (which relays TCP) cannot carry it. To proxy HTTP/3, use a SOCKS5 proxy that supports UDP relay (SOCKS5 UDP ASSOCIATE, RFC 1928 §7). When an HTTP/3 request is paired with a socks5:// proxy, the client automatically negotiates a UDP relay with the proxy and tunnels QUIC through it — no special scheme or extra option is needed:
const session = requests.session({
httpVersion: "http3_only", // or "http3"
proxy: "socks5://user:pass@host:port", // a socks5 proxy with UDP relay
http3Fingerprint: "auto", // QUIC fingerprint still applies
});
const resp = await session.get("https://fp.impersonate.pro/api/http3");
console.log(resp.json.protocol); // "http3"The proxy object form and setProxy work too:
session.setProxy({
proxy: "socks5://host:port",
username: "user",
password: "pass",
});Same for fetch:
await fetch("https://example.com", {
httpVersion: "http3_only",
proxy: "socks5://user:pass@host:port",
});Behavior notes:
http3_onlyfails hard (throwsCouldn't connect to server) if the proxy has no working UDP relay.http3falls back to TCP (HTTP/1.1/2 through the proxy) when the UDP relay fails — the response will not be HTTP/3.- Very few proxy vendors support SOCKS5 UDP relay (most are HTTP/HTTPS-only or SOCKS5-TCP-only; those that support UDP typically ship it as a beta feature) — confirm with your provider before relying on it.
Response
| Property | Type | Description |
| :------- | :--- | :---------- |
| status | number | HTTP status code. |
| text | string | Decoded response body. |
| json | object | JSON.parse(text). |
| buffer | Uint8Array | Raw response body. |
| headers | string | Raw response headers. |
| headersMap | Headers | Response headers as a Headers object. |
| contentLength | number | Response body length. |
| encodedBodySize | number | Wire-level (pre-decompression) body size. |
2. fetch (fetch-style)
import { fetch } from "@ossiana/node-libcurl";
const resp = await fetch("https://httpbin.org/post", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: { hello: "world" }, // object is JSON.stringify'd
redirect: true,
ja3: "chrome150",
akamai: "auto",
httpVersion: "http3",
});
await resp.text(); // Promise<string>
await resp.json(); // Promise<object>
await resp.arraybuffer(); // Promise<ArrayBuffer>
await resp.headers(); // Promise<Headers>
await resp.cookies(); // Promise<string>
await resp.cookiesMap(); // Promise<Map<string, {...}>>
await resp.lastEffectiveUrl(); // Promise<string>
resp.status(); // number (sync)
resp.contentLength();
resp.encodedBodySize();Options (all optional): method (default "GET"), headers, body, redirect, cookies, httpVersion, verbose, proxy, connectTo, timeout, interface, instance, ja3, akamai, autoSortRequestHeaders, sslCert, sslVerify, tlsVerifySigalgs, http3Fingerprint (default "auto").
Pass a shared LibCurl instance via instance to keep a persistent connection across calls:
const curl = new LibCurl();
const r1 = await fetch("https://example.com/a", { instance: curl });
const r2 = await fetch("https://example.com/b", { instance: curl }); // reuses the connection3. Fingerprints
This is the core feature — make your requests indistinguishable from a real browser.
JA3 (TLS fingerprint)
ja3: "auto" // pick by User-Agent Chrome version (default)
ja3: "chrome99" | "chrome101" | "chrome110" | "chrome124" | "chrome131" | "chrome133" | "chrome150" | "chrome152"
ja3: "771,4865-4866-4867-...,0-23-65281-10-11-35-16-...,29-23-24,0" // custom JA3 stringThe built-in versions randomize the TLS extension order per request (like real Chrome), and "auto" selects a preset matching the Chrome/x.y version in your User-Agent header.
Akamai (HTTP/2 fingerprint)
akamai: "auto" // default
akamai: "chrome99" | "chrome107" | "chrome119"
akamai: "1:65536;3:1000;4:6291456;6:262144|15663105|0|m,a,s,p" // custom stringHTTP/3 fingerprint
http3Fingerprint: "auto" // default
http3Fingerprint: "chrome126" | "chrome150" | "chrome152"
http3Fingerprint: { // fully custom config
scid: "scid=0",
settings: "1:65536;6:262144;7:100;51:1;GREASE",
transport_params: "12584:0x4f524947;9:103;1:30000;7:6291456;15:AUTO;4:15728640;GREASE;32:65536;3:1472;17:1@1,GREASE;8:100;6:6291456;12583:174718;5:6291456",
tls: "ciphers=1,2,3;alps=h3;grease=off;rand=on",
permutation: "0,15,19,23,9,1,14,21,17,4,7,27",
verify_sigalgs: "0x0403,0x0804,0x0401,0x0503,0x0805,0x0501,0x0806,0x0601,0x0201",
trust_anchors: "44947.2.14,11129.9.8,...", // optional: trust_anchors (0xCA34) IDs; empty = built-in Chrome 152 list
},Proxied HTTP/3 needs a SOCKS5 proxy with UDP relay — see HTTP/3 through a SOCKS5 UDP proxy.
TLS signature algorithms (HTTP/1.1 HTTP/2)
tlsVerifySigalgs: [
0x0403, "ecdsa_secp256r1_sha256", "rsa_pss_rsae_sha256", "rsa_pkcs1_sha256", ...
]Auto-sorted request headers
With autoSortRequestHeaders: "auto" (default), request headers are automatically re-ordered the same way Chrome's fetch does (prefix / client-hint / middle / suffix groups). Use "chrome130" / "chrome131" to pin a specific ordering version.
4. LibCurl (low-level)
import { LibCurl } from "@ossiana/node-libcurl";
const curl = new LibCurl();
curl.open("POST", "https://example.com/api");
curl.setRequestHeaders({ "Content-Type": "application/json" });
curl.setJA3Fingerprint("chrome150");
curl.setAkamaiFingerprint("auto");
curl.setHttp3Fingerprint("auto");
curl.setProxy("127.0.0.1:8888"); // or { proxy, username, password }
curl.setConnectTo("foo.abc.com:443:static.abc.com:443"); // connect to static.abc.com instead
curl.setTimeout(10, 20); // connect / total, seconds
curl.setRedirect(true);
curl.setHttpVersion("http2");
curl.setInterface("eth0");
curl.setVerbose(true); // print curl internal logs
await curl.send({ hello: "world" }); // object is JSON.stringify'd
curl.getResponseStatus(); // number
curl.getResponseHeaders(); // string
curl.getResponseHeadersMap(); // Headers
curl.getResponseString(); // string
curl.getResponseBody(); // Uint8Array
curl.getResponseContentLength();
curl.getResponseEncodedBodySize();
curl.getCookies(); // "a=b; c=d;"
curl.getLastEffectiveUrl();Other setters: setRequestHeader(key, value), setCookie({name, value, domain, path}), getCookie({name, domain, path}), getCookiesMap(), deleteCookie(), setSSLVerify({caPath}), setSSLCert(certBlob, privateKeyBlob?, "PEM"|"DER"|"P12", password?), setTLSVerifySigalgs(), setHttp2NextStreamId(streamId), setHttp2StreamWeight(weight), setAutoSortRequestHeaders(), setRequestType("fetch"|"XMLHttpRequest"), setNextRequestType(), setNextRequestHeadersOrder(order).
Note: one
LibCurlinstance can only run one request at a time — callingsend()while a request is in flight throws.
5. WebSocket
const ws = new LibCurlWebSocket("wss://echo.websocket.org", {
userAgent: "Mozilla/5.0 ... Chrome/150.0.0.0 Safari/537.36",
origin: "https://example.com",
cookie: "session=abc",
protocol: "chat",
timeout: 30,
ja3: "chrome150",
// instance: sharedCurl, // reuse an existing LibCurl
});
ws.onopen = () => {
ws.send("hello");
ws.send(new Uint8Array([1, 2, 3]));
};
ws.onmessage = (data: Uint8Array) => { /* receive */ };
ws.onclose = () => {};
ws.onerror = (message: string) => {};
ws.close();Difference from Node.js fetch
- The TLS fingerprint (JA3), HTTP/2 (Akamai) and HTTP/3 fingerprints can be customized to look like Chrome or Firefox — node-libcurl patches BoringSSL and ships a custom HTTP/2/3 stack, so the handshake, header ordering and cipher suites match real browsers.
- Request headers can be auto-sorted exactly like Chrome's
fetch(autoSortRequestHeaders). - Persistent connections and cookie jars are first-class (session API).
- Client certificates, custom CA, per-interface binding, custom TLS signature algorithms, and HTTP/2 stream weight control are all supported.
