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'); Cookie Jar
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
| Feature | Rezo | Undici |
|---|---|---|
| Node.js | Yes | Yes |
| Browsers | Yes | No |
| Deno / Edge | Yes | No |
| React Native | Yes | No |
| HTTP/1.1 | Yes | Yes |
| HTTP/2 | Yes | Yes (experimental) |
| Auto JSON parsing | Yes | No — manual .json() |
| Throws on HTTP errors | Yes | No |
| Cookie jar | Built-in | No |
| Cookie persistence | JSON and Netscape | No |
| Retry | Full (backoff, conditions, status codes) | RetryAgent (basic) |
| Staged timeouts | Full (connect, headers, body, total) | Partial (headers, body) |
| Interceptors | Request and response | No |
| Hooks | 26 lifecycle hooks | No |
| Proxy rotation | Built-in with health monitoring | No |
| SOCKS proxy | Built-in | No |
| Stealth mode | 18 browser profiles | No |
| Progress events | Downloads and uploads | No |
| Request queue | Built-in with rate limiting | No |
| Response caching | Built-in | No |
| DNS caching | Built-in | No |
| Web crawler | Built-in | No |
| Site cloning | Built-in | No |
| cURL adapter | Yes | No |
| Error codes | 70+ with recovery suggestions | ~10 |
| TypeScript | Strict types, generics, overloads | Yes |