Switch to Rezo

From Undici

Undici is a high-performance HTTP/1.1 client for Node.js, built as the engine behind Node’s built-in fetch(). It is fast and low-level — ideal when raw throughput is the only requirement. Rezo delivers comparable performance through its HTTP adapter while providing a complete feature set: multi-runtime support, cookies, proxy rotation, stealth, hooks, streaming with progress events, and structured error handling.

Basic GET

// Undici
import { request } from 'undici';

const { statusCode, headers, body } = await request('https://api.example.com/users');
const data = await body.json();

// Rezo -- JSON parsed automatically
import rezo from 'rezo';

const { data, status, headers } = await rezo.get('https://api.example.com/users');

POST with JSON

// Undici
import { request } from 'undici';

const { body } = await request('https://api.example.com/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Ada Lovelace' }),
});
const data = await body.json();

// Rezo
const { data } = await rezo.postJson('https://api.example.com/users', {
  name: 'Ada Lovelace',
});

Headers

// Undici -- headers as an object or array of key-value pairs
const { body } = await request('https://api.example.com/data', {
  headers: {
    'Authorization': 'Bearer token123',
    'Accept': 'application/json',
  },
});

// Rezo -- same pattern, RezoHeaders instance available on response
const { data, headers } = await rezo.get('https://api.example.com/data', {
  headers: {
    'Authorization': 'Bearer token123',
    'Accept': 'application/json',
  },
});

// RezoHeaders provides typed access
headers.get('content-type');  // "application/json"

Body Consumption

Undici requires explicit body consumption. If you forget, the connection leaks:

// Undici -- must consume or destroy body
const { statusCode, body } = await request('https://api.example.com/data');

// Must consume the body
const data = await body.json();
// or: await body.text()
// or: await body.arrayBuffer()
// or: body.dump() to discard

// Rezo -- body is consumed and parsed automatically
const { data, status } = await rezo.get('https://api.example.com/data');
// data is already parsed based on Content-Type

Error Handling

// Undici -- errors are low-level
import { request, errors } from 'undici';

try {
  await request('https://api.example.com/data');
} catch (error) {
  if (error instanceof errors.ConnectTimeoutError) {
    console.log('Connection timed out');
  } else if (error instanceof errors.SocketError) {
    console.log('Socket error');
  }
  // HTTP status errors require manual checking -- undici does not throw on 4xx/5xx
}

// Rezo -- unified error handling
import rezo, { RezoError } from 'rezo';

try {
  await rezo.get('https://api.example.com/data');
} catch (error) {
  if (rezo.isRezoError(error)) {
    console.log(error.code);           // "REZ_TIMEOUT", "REZ_HTTP_ERROR", etc.
    console.log(error.status);         // 503
    console.log(error.isTimeout);      // false
    console.log(error.isServerError);  // true
    console.log(error.suggestion);     // "The server returned a 503..."
  }
}

Timeouts

// Undici
import { request } from 'undici';

const { body } = await request('https://api.example.com/data', {
  headersTimeout: 10_000,
  bodyTimeout: 30_000,
});

// Rezo -- staged timeouts with connect phase
const { data } = await rezo.get('https://api.example.com/data', {
  timeout: {
    connect: 5_000,
    headers: 10_000,
    body: 30_000,
    total: 60_000,
  }
});

Connection Pooling

// Undici -- explicit pool management
import { Pool } from 'undici';

const pool = new Pool('https://api.example.com', {
  connections: 10,
  pipelining: 1,
});

const { body } = await pool.request({ path: '/users', method: 'GET' });
const data = await body.json();

pool.close();

// Rezo -- connection management is automatic
import rezo from 'rezo';

const client = rezo.create({
  baseURL: 'https://api.example.com',
  queueOptions: {
    enable: true,
    options: { concurrency: 10 },
  }
});

const { data } = await client.get('/users');
// Connections managed internally

Proxy

// Undici
import { ProxyAgent, request } from 'undici';

const proxyAgent = new ProxyAgent('http://proxy:8080');
const { body } = await request('https://example.com', {
  dispatcher: proxyAgent,
});

// Rezo -- built-in proxy with multiple protocols
const { data } = await rezo.get('https://example.com', {
  proxy: 'http://proxy:8080',
});

// Or SOCKS5
const { data: data2 } = await rezo.get('https://example.com', {
  proxy: 'socks5://proxy:1080',
});

Retry

Undici has built-in retry via RetryAgent, but configuration is limited:

// Undici
import { RetryAgent, Agent } from 'undici';

const agent = new RetryAgent(new Agent(), {
  maxRetries: 3,
  minTimeout: 500,
  maxTimeout: 10_000,
});

// Rezo -- full retry control
const { data } = await rezo.get('https://api.example.com/data', {
  retry: {
    limit: 3,
    backoff: { delay: 500, maxDelay: 10_000 },
    statusCodes: [429, 500, 502, 503, 504],
    methods: ['GET', 'POST', 'PUT'],
    condition: (error, attempt) => {
      return attempt < 3 && error.isServerError;
    },
  }
});

What You Gain by Switching

Multi-Runtime Support

Undici is Node.js-only. Rezo runs on Node.js, Bun, Deno, browsers, React Native, and edge runtimes.

// Same code runs everywhere
import rezo from 'rezo';
const { data } = await rezo.get('https://api.example.com/users');

Undici does not manage cookies. Rezo includes a full cookie jar with persistence:

import { Rezo, CookieJar } from 'rezo';

const jar = new CookieJar();
const client = rezo.create({ jar });

await client.postJson('https://example.com/login', { user: 'ada', pass: 'secret' });
const { data } = await client.get('https://example.com/dashboard');

// Persist cookies
const json = jar.toJSON();
const netscape = jar.toNetscape();

Proxy Rotation

import { Rezo, ProxyManager } from 'rezo';

const client = rezo.create({
  proxyManager: new ProxyManager([
    'http://proxy1:8080',
    'socks5://proxy2:1080',
    'https://proxy3:3128',
  ]),
});
// Automatic rotation with health monitoring

Stealth Mode

import rezo from 'rezo';
import { RezoStealth } from 'rezo/stealth';

const client = rezo.create({
  stealth: new RezoStealth('chrome-131'),
});

18 browser profiles with TLS fingerprinting, header ordering, and HTTP/2 SETTINGS emulation.

26 Lifecycle Hooks

const client = rezo.create({
  hooks: {
    beforeRequest: [(config) => { /* ... */ }],
    afterResponse: [(res) => { return res; }],
    beforeRetry: [(config, err, ctx) => { /* ... */ }],
    afterCookie: [(cookies, config) => { /* ... */ }],
  }
});

Streaming with Progress Events

await rezo.download('https://example.com/large-file.zip', {
  outputPath: './file.zip',
  onProgress: ({ percent, transferred, total }) => {
    console.log(`${percent}%`);
  }
});

cURL Adapter

import rezo from 'rezo';
import curlAdapter from 'rezo/adapters/curl';

const client = rezo.create({ adapter: curlAdapter });

Feature Comparison

FeatureRezoUndici
Node.jsYesYes
BrowsersYesNo
Deno / EdgeYesNo
React NativeYesNo
HTTP/1.1YesYes
HTTP/2YesYes (experimental)
Auto JSON parsingYesNo — manual .json()
Throws on HTTP errorsYesNo
Cookie jarBuilt-inNo
Cookie persistenceJSON and NetscapeNo
RetryFull (backoff, conditions, status codes)RetryAgent (basic)
Staged timeoutsFull (connect, headers, body, total)Partial (headers, body)
InterceptorsRequest and responseNo
Hooks26 lifecycle hooksNo
Proxy rotationBuilt-in with health monitoringNo
SOCKS proxyBuilt-inNo
Stealth mode18 browser profilesNo
Progress eventsDownloads and uploadsNo
Request queueBuilt-in with rate limitingNo
Response cachingBuilt-inNo
DNS cachingBuilt-inNo
Web crawlerBuilt-inNo
Site cloningBuilt-inNo
cURL adapterYesNo
Error codes70+ with recovery suggestions~10
TypeScriptStrict types, generics, overloadsYes