@eric8810/catcher-napi-http
v0.3.19
Published
catcher HTTP native addon for Node.js via napi-rs
Readme
@eric8810/catcher-napi-http
Rust-powered HTTP + SSE client for Node.js via napi-rs. Part of the catcher toolkit.
Wraps catcher-http's HttpTransport — reqwest + retry + circuit breaker, compiled to a native addon. Includes typed TypeScript wrappers with auto-generated .d.ts.
⚠️ Breaking Changes (0.3.0+)
Migrate from 0.2.x → 0.3.x — see napi API docs for full details.
v0.3.2:
HttpResponse,RequestOptions,Metricstypes now correctly use camelCase (auto-generated by NAPI-RS). JSON config types (HttpClientConfig, etc.) remain snake_case.
| Change | Before | After |
|--------|--------|-------|
| Entry point | client.js / client.d.ts | dist/client.js / dist/client.d.ts |
| Config format | JSON.stringify(config) only | Typed object or JSON string |
| Class names | JsHttpClient, JsSseStream, JsSseClient | HttpClient, SseStream, SseClient |
| Callback events | Raw JSON strings, need JSON.parse() | Typed objects, auto-parsed |
| Default backoff | Exponential | Fixed |
| Default connect_timeout_ms | 5000 | 10000 |
| Default min_backoff_ms | 500 | 100 |
| Default max_backoff_ms | 30000 | 10000 |
| camelCase fields | Not supported | #[serde(alias)] — both snake_case and camelCase accepted |
- const client = require('@eric8810/catcher-napi-http').HttpClient
- const c = new HttpClient(JSON.stringify({ base_url: '...' }))
+ import { HttpClient } from '@eric8810/catcher-napi-http'
+ const c = new HttpClient({ base_url: '...' })Install
npm install @eric8810/catcher-napi-httpPre-built binaries available for Linux (x64/arm64 gnu/musl), macOS (x64/arm64), and Windows (x64/arm64). Installed automatically via platform-specific optionalDependencies.
Usage
import { HttpClient, HttpError } from '@eric8810/catcher-napi-http'
import type { HttpClientConfig, SseEvent } from '@eric8810/catcher-napi-http/types'
// Config as typed object (recommended) or JSON string
const client = new HttpClient({
base_url: 'https://api.example.com',
connect_timeout_ms: 10000,
retry: { max_attempts: 3, backoff: 'Fixed' },
circuit_breaker: { failure_threshold: 5, reset_timeout_ms: 30000 },
dns: {
mode: 'catcher',
cache_size: 512,
cache_ttl_secs: 300,
negative_ttl_secs: 60,
stale_ttl_secs: 3600,
stale_on_error: true,
host_mapping: { 'api.internal': '10.0.0.10' },
},
msgpack: true,
})
// GET
const resp = await client.get('/users/1')
console.log(resp.status, resp.body.toString())
// POST
await client.post('/messages', Buffer.from(JSON.stringify({ text: 'hello' })), {
contentType: 'application/json',
})
try {
await client.get('/missing')
} catch (error) {
if (error instanceof HttpError) {
console.log(error.status, error.body)
}
}
// Circuit breaker state
console.log(client.circuitBreakerState()) // 'closed' | 'open' | 'half-open'
// SSE (one-shot stream)
import { SseStream } from '@eric8810/catcher-napi-http/sse'
const stream = new SseStream(
{ url: 'https://stream.example.com/events' },
(event: SseEvent) => {
if (event.type === 'Line') console.log(event.data)
},
)
// later: stream.close()
// SSE (auto-reconnect client)
import { SseClient } from '@eric8810/catcher-napi-http/sse'
const sse = new SseClient(
{
url: 'https://stream.example.com/events',
reconnect: { max_retries: 10, initial_delay_ms: 1000 },
},
(event: SseEvent) => {
if (event.type === 'Line') console.log(event.data)
},
)API
new HttpClient(config: HttpClientConfig | string)
Create a client from a typed config object or JSON string. All fields are optional with sensible defaults. Supports both snake_case and camelCase field names.
interface HttpClientConfig {
base_url?: string
connect_timeout_ms?: number // default: 10000
response_timeout_ms?: number // default: 30000
pool?: PoolConfig
tls?: TlsConfig
dns?: DnsConfig
retry?: RetryConfig
circuit_breaker?: CircuitBreakerConfig
max_concurrency?: number // default: 50
default_headers?: Record<string, string>
hostname_override?: string
proxy?: ProxyConfig
redirect?: RedirectConfig
auth?: { username: string; password: string }
bearer_token?: string
msgpack?: boolean // default: false
}
interface DnsConfig {
mode?: 'catcher' | 'native' // default: 'catcher'
cache_size?: number // default: 512
cache_ttl_secs?: number // default: 300
negative_ttl_secs?: number // default: 60
stale_ttl_secs?: number // default: 3600
stale_on_error?: boolean // default: true
nameservers?: string[]
host_mapping?: Record<string, string>
}
type ProxyConfig =
| { mode?: 'manual'; url: string; auth?: ProxyAuth; no_proxy?: string[] }
| { mode: 'direct' }
| { mode: 'system'; no_proxy?: string[] }When msgpack is enabled, JSON request bodies are encoded as MessagePack and MessagePack responses are decoded back to JSON bytes when the response content type contains msgpack.
mode: 'direct' calls reqwest's ClientBuilder::no_proxy() and therefore ignores
explicit, environment, and automatic system proxies. mode: 'system' detects fixed
system proxies after networkChanged(); PAC/WPAD scripts must be resolved by the host
application and passed as a manual proxy because Catcher does not embed a PAC engine.
HTTP responses with status >= 400 reject with HttpError, which exposes structured
status and body fields. A 421 Misdirected Request additionally retires this
client's connection pool and retries once on a fresh connection. Other clients and
in-flight requests are not cancelled.
Native transport failures reject with CatcherError instead of the N-API default
GenericFailure. The error exposes a stable code, failure phase, retryable
flag, and structured details. Retry exhaustion includes both the total attempt
count (the initial request plus retries actually executed) and the final structured
transport error in details.lastError, including its own code, phase, retryable,
and details; request URLs are stripped before the native cause chain is serialized.
import { CatcherError } from '@eric8810/catcher-napi-http'
try {
await client.get('/users/1')
} catch (error) {
if (error instanceof CatcherError) {
console.log(error.code, error.phase, error.details)
}
}Methods
| Method | Signature |
|--------|-----------|
| get(url, options?) | async (url: string, options?: RequestOptions) => HttpResponse |
| post(url, body?, options?) | async (url: string, body?: Buffer, options?: RequestOptions) => HttpResponse |
| put(url, body?, options?) | async (url: string, body?: Buffer, options?: RequestOptions) => HttpResponse |
| delete(url, options?) | async (url: string, options?: RequestOptions) => HttpResponse |
| patch(url, body?, options?) | async (url: string, body?: Buffer, options?: RequestOptions) => HttpResponse |
| circuitBreakerState() | () => 'closed' \| 'open' \| 'half-open' |
| metrics() | () => Metrics |
| executeStream(method, url, body?, options?, onChunk?) | (method: string, url: string, body?: Buffer, options?: RequestOptions, onChunk?: (event: StreamEvent) => void) => void |
| setAdaptiveTimeout(min, max, mult, win) | (min: number, max: number, mult: number, win: number) => void |
| cancelAll() | () => void |
| cancelRequest(requestId) | (requestId: number) => boolean |
| nextRequestId() | () => number |
RequestOptions
Per-request options (NAPI-RS auto-generated, camelCase fields):
interface RequestOptions {
headers?: Record<string, string>
timeoutMs?: number
contentType?: string
}HttpResponse
NAPI-RS auto-generated (camelCase fields):
interface HttpResponse {
status: number
headers: Record<string, string>
body: Buffer
elapsedMs: number
}Metrics
Runtime metrics snapshot (NAPI-RS auto-generated, camelCase fields):
interface Metrics {
httpRequests: number
httpSuccessRate: number
httpAvgLatencyUs: number
httpRetries: number
wsConnectSuccessRate: number
wsDisconnects: number
wsMessagesSent: number
wsMessagesReceived: number
cbOpenCount: number
queueTimeouts: number
}Note:
HttpResponse,RequestOptions, andMetricsare auto-generated by NAPI-RS (camelCase fields). JSON config types likeHttpClientConfiguse snake_case (serde-based).
SSE
| Class | Description |
|-------|-------------|
| SseStream | One-shot SSE stream (no auto-reconnect) |
| SseClient | Long-lived SSE client with auto-reconnect |
new SseStream(config: SseClientConfig | string, onEvent: (event: SseEvent) => void)
new SseClient(config: SseClientConfig | string, onEvent: (event: SseEvent) => void)
type SseEvent =
| { type: 'Line'; data: string }
| { type: 'Error'; message: string }
| { type: 'End' }StreamEvent
type StreamEvent =
| { type: 'Headers'; status: number; headers: Record<string, string> }
| { type: 'Chunk'; data: string } // base64 encoded
| { type: 'Done' }
| { type: 'Error'; message: string }Build from Source
Requires Rust toolchain.
npm run build # napi build + tsup compile
npm run build:ts # tsup only (no Rust rebuild)License
MIT
