stealth-io
v1.0.2
Published
stealth-io
Maintainers
Readme
- Motivation
- Features
- Requirements
- Installation
- Loading and configuring the module
- Quick Start
- Common Usage
- Advanced Usage
- API
- Middleware
- Testing
- Architecture
- Team
- Contributing
- License
Motivation
Why build another HTTP client when so many exist? Because sometimes you need full control without the baggage of external dependencies. Stealth-IO is built entirely from Node.js core modules (http, https, url, stream, zlib, events), making it:
- Lightweight: Zero external runtime dependencies
- Secure: No supply chain vulnerabilities from third-party packages
- Predictable: Behavior determined solely by Node.js internals
- Portable: Works anywhere Node.js runs
Features
- Zero Dependencies: Built entirely from Node.js core modules
- Full HTTP Method Support: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS
- Automatic JSON Handling: Serialize/deserialize JSON request and response bodies
- Response Body Processing: Support for JSON, text, buffer, and stream responses
- Middleware Pipeline: Koa-style onion model for request/response interception
- Configurable Retry Logic: Exponential backoff with jitter support
- Pattern-Based Retry: Retry on response body/header patterns (unique feature)
- Rate Limiting: Built-in rate limiter with queue support for API integrations
- Request Timeout: Configurable timeout with proper cleanup
- Abort/Cancellation: Full AbortController/AbortSignal support
- Automatic Redirects: Follow redirects with configurable limits
- Response Decompression: Automatic gzip, deflate, and brotli decompression
- Case-Insensitive Headers: Headers class with normalized key access
- Query String Handling: Parse and serialize query parameters
- Connection Pooling: Leverage Node.js built-in HTTP agents
- Pluggable Transport: Custom transport interface for advanced use cases
Requirements
- Node.js >= 18.0.0
Installation
npm install stealth-ioLoading and configuring the module
ES Modules (ESM)
import stealthIO from 'stealth-io';Named Exports
import { StealthClient, createClient, Headers } from 'stealth-io';CommonJS
Stealth-IO is an ESM-only module. For CommonJS projects, use dynamic import:
const stealthIO = (...args) => import('stealth-io').then(({default: stealthIO}) => stealthIO(...args));Quick Start
import stealthIO from 'stealth-io';
const response = await stealthIO.get('https://api.example.com/users');
const data = await response.json();
console.log(data);Common Usage
Plain text or HTML
import stealthIO from 'stealth-io';
const response = await stealthIO.get('https://github.com/');
const body = await response.text();
console.log(body);JSON
import stealthIO from 'stealth-io';
const response = await stealthIO.get('https://api.github.com/users/github');
const data = await response.json();
console.log(data);Simple Post
import stealthIO from 'stealth-io';
const response = await stealthIO.post('https://httpbin.org/post', 'a=1');
const data = await response.json();
console.log(data);Post with JSON
import stealthIO from 'stealth-io';
const response = await stealthIO.post('https://httpbin.org/post', {
name: 'John Doe',
email: '[email protected]'
});
const data = await response.json();
console.log(data);Post with form parameters
import stealthIO from 'stealth-io';
const params = new URLSearchParams();
params.append('username', 'john');
params.append('password', 'secret');
const response = await stealthIO.post('https://httpbin.org/post', params.toString(), {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
const data = await response.json();
console.log(data);Handling exceptions
import stealthIO, { TimeoutError, NetworkError, AbortError } from 'stealth-io';
try {
const response = await stealthIO.get('https://domain.invalid/');
} catch (error) {
if (error instanceof TimeoutError) {
console.log('Request timed out:', error.timeout);
} else if (error instanceof NetworkError) {
console.log('Network error:', error.code);
} else if (error instanceof AbortError) {
console.log('Request was aborted');
} else {
console.log('Unknown error:', error);
}
}Handling client and server errors
import stealthIO, { HTTPError } from 'stealth-io';
try {
const response = await stealthIO.get('https://httpbin.org/status/404');
} catch (error) {
if (error instanceof HTTPError) {
console.log('HTTP Error:', error.status, error.statusText);
const body = await error.response.text();
console.log('Response body:', body);
}
}Or disable automatic status validation:
import stealthIO from 'stealth-io';
const response = await stealthIO.get('https://httpbin.org/status/404', {
validateStatus: false
});
if (!response.ok) {
console.log('Request failed with status:', response.status);
}Advanced Usage
Creating a Client Instance
For repeated requests to the same API, create a client instance with shared configuration:
import { StealthClient, createClient } from 'stealth-io';
const client = new StealthClient({
baseURL: 'https://api.example.com',
timeout: 10000,
headers: {
'Authorization': 'Bearer your-token',
'X-API-Key': 'your-api-key'
}
});
const users = await client.get('/users');
const user = await client.post('/users', { name: 'John' });Or use the factory function:
const client = createClient({
baseURL: 'https://api.example.com',
timeout: 10000
});Streams
Stream responses for large files or real-time data:
import { createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';
import stealthIO from 'stealth-io';
const response = await stealthIO.get('https://example.com/large-file.zip');
if (!response.ok) throw new Error(`Unexpected response: ${response.statusText}`);
await pipeline(response.stream(), createWriteStream('./large-file.zip'));Accessing Headers and other Metadata
import stealthIO from 'stealth-io';
const response = await stealthIO.get('https://github.com/');
console.log(response.ok);
console.log(response.status);
console.log(response.statusText);
console.log(response.url);
console.log(response.headers.get('content-type'));
console.log(response.headers.entries());Request cancellation with AbortSignal
import stealthIO, { AbortError } from 'stealth-io';
const controller = new AbortController();
setTimeout(() => controller.abort(), 150);
try {
const response = await stealthIO.get('https://example.com/slow-endpoint', {
signal: controller.signal
});
const data = await response.json();
} catch (error) {
if (error instanceof AbortError) {
console.log('Request was aborted');
}
}Using the built-in abort controller:
import { StealthClient } from 'stealth-io';
const client = new StealthClient();
const controller = client.createAbortController();
const promise = client.get('https://example.com/slow-endpoint', {
signal: controller.signal
});
setTimeout(() => controller.abort(), 1000);
try {
const response = await promise;
} catch (error) {
if (error.code === 'ABORTED') {
console.log('Request was cancelled');
}
}Middleware
Add middleware to intercept requests and responses:
import { StealthClient, createLoggingMiddleware, createAuthMiddleware } from 'stealth-io';
const client = new StealthClient({
baseURL: 'https://api.example.com'
});
client.use(createLoggingMiddleware({
logger: console.log
}));
client.use(createAuthMiddleware({
type: 'bearer',
token: 'your-token'
}));
const response = await client.get('/users');Retry Logic
Configure automatic retries with exponential backoff:
import { StealthClient, createRetryPolicy } from 'stealth-io';
const client = new StealthClient({
maxRetries: 3,
retryDelay: 1000,
retryStrategy: 'exponential_jitter'
});
const response = await client.get('/flaky-endpoint');Using preset retry policies:
import { StealthClient, createRetryPolicy } from 'stealth-io';
const client = new StealthClient({
...createRetryPolicy('aggressive')
});Available policies:
aggressive: 5 retries, 500ms delay, exponential jittermoderate: 3 retries, 1000ms delay, exponentialconservative: 2 retries, 2000ms delay, exponentialnone: No retries
Rate Limiting
Built-in rate limiting with queue support for API integrations:
import { createClient } from 'stealth-io';
const client = createClient({
rateLimit: {
maxRequests: 50,
perMs: 1000,
queue: true
}
});
const response = await client.get('https://api.example.com/data');Alternative time-based configurations:
const client = createClient({
rateLimit: { perSecond: 10 }
});
const client = createClient({
rateLimit: { perMinute: 60 }
});
const client = createClient({
rateLimit: { perHour: 1000 }
});Full rate limit options:
const client = createClient({
rateLimit: {
maxRequests: 50,
perMs: 1000,
queue: true,
maxQueueSize: 100,
queueTimeout: 60000,
algorithm: 'sliding-window',
burstLimit: null,
onThrottle: ({ waitTime, queueSize, queued }) => {
console.log(`Rate limited. Wait: ${waitTime}ms`);
},
onDequeue: ({ waitedMs, remainingQueue }) => {
console.log(`Request dequeued after ${waitedMs}ms`);
}
}
});| Option | Type | Default | Description |
|--------|------|---------|-------------|
| maxRequests | number | 50 | Maximum requests per time window |
| perMs | number | 1000 | Time window in milliseconds |
| perSecond | number | - | Shorthand for requests per second |
| perMinute | number | - | Shorthand for requests per minute |
| perHour | number | - | Shorthand for requests per hour |
| queue | boolean | false | Queue requests instead of rejecting |
| maxQueueSize | number | 100 | Maximum queue size |
| queueTimeout | number | 60000 | Queue timeout in milliseconds |
| algorithm | string | 'sliding-window' | Algorithm: 'sliding-window' or 'token-bucket' |
| burstLimit | number | null | Burst limit for token bucket algorithm |
| onThrottle | function | null | Callback when rate limited |
| onDequeue | function | null | Callback when request is dequeued |
Accessing the rate limiter:
const limiter = client.getRateLimiter();
console.log(limiter.getRemainingRequests());
console.log(limiter.getQueueSize());
client.setRateLimit({ perSecond: 20 });
client.setRateLimit(null);Pattern-Based Retry
Retry requests based on response body or header patterns:
import { createClient } from 'stealth-io';
const client = createClient({
retry: {
maxRetries: 5,
matchBody: /temporarily unavailable/i
}
});Full pattern retry options:
const client = createClient({
retry: {
maxRetries: 5,
retryDelay: 1000,
maxRetryDelay: 30000,
retryStrategy: 'exponential',
matchBody: [/temporarily unavailable/i, /rate limit/i],
matchHeaders: { 'x-ratelimit-remaining': '0' },
matchStatus: [429, 500, 502, 503, 504],
matchAny: true,
excludeBody: /permanent error/i,
excludeHeaders: { 'x-no-retry': 'true' },
excludeStatus: [404, 401],
onRetry: ({ response, attempt, delay, reason }) => {
console.log(`Retry ${attempt + 1}, reason: ${reason}`);
},
onPatternMatch: ({ response, attempt, result }) => {
console.log(`Pattern matched: ${result.reason}`);
}
}
});| Option | Type | Default | Description |
|--------|------|---------|-------------|
| maxRetries | number | 3 | Maximum retry attempts |
| retryDelay | number | 1000 | Base retry delay in milliseconds |
| maxRetryDelay | number | 30000 | Maximum retry delay |
| retryStrategy | string | 'exponential' | Backoff strategy |
| matchBody | RegExp\|string\|function\|array | - | Body patterns to trigger retry |
| matchHeaders | object | - | Header patterns to trigger retry |
| matchStatus | number\|array\|Set\|function | - | Status codes to trigger retry |
| matchAny | boolean | false | Retry if any pattern matches (default: all must match) |
| excludeBody | RegExp\|string\|function\|array | - | Body patterns to prevent retry |
| excludeHeaders | object | - | Header patterns to prevent retry |
| excludeStatus | number\|array\|Set | - | Status codes to prevent retry |
| onRetry | function | - | Callback on retry |
| onPatternMatch | function | - | Callback when pattern matches |
Pre-built patterns for common APIs:
import { createClient, createApiSpecificPatterns } from 'stealth-io';
const discordPatterns = createApiSpecificPatterns('discord');
const client = createClient({
retry: {
maxRetries: 5,
...discordPatterns
}
});Supported APIs: discord, twitter, shopify, github, stripe
Common pattern matchers:
import { createCommonPatternMatchers } from 'stealth-io';
const patterns = createCommonPatternMatchers();
const client = createClient({
retry: {
maxRetries: 3,
matchBody: [
patterns.temporarilyUnavailable,
patterns.rateLimited,
patterns.maintenance,
patterns.overloaded
],
matchAny: true
}
});Available patterns:
temporarilyUnavailable- Matches "temporarily unavailable"rateLimited- Matches "rate limit", "too many requests", "throttle"maintenance- Matches "maintenance", "down for maintenance"overloaded- Matches "overloaded", "capacity", "try again later"serviceUnavailable- Matches "service unavailable", "503"timeout- Matches "timeout", "timed out"internalError- Matches "internal error", "unexpected error"badGateway- Matches "bad gateway", "502"gatewayTimeout- Matches "gateway timeout", "504"connectionError- Matches "connection refused", "connection reset"retryLater- Matches "retry", "try again", "come back later"
API
stealthIO(url[, options])
urlA string representing the URL for the requestoptionsOptions for the HTTP(S) request- Returns:
Promise<StealthResponse>
Perform an HTTP(S) request.
Options
The default values are shown after each option key.
{
method: 'GET',
headers: {},
body: null,
timeout: 30000,
followRedirects: true,
maxRedirects: 5,
validateStatus: true,
maxRetries: 0,
retryDelay: 1000,
retryStrategy: 'exponential',
responseType: 'auto',
signal: null,
params: {},
auth: null,
baseURL: null,
rateLimit: null,
retry: null
}| Option | Type | Default | Description |
|--------|------|---------|-------------|
| method | string | 'GET' | HTTP method |
| headers | object | {} | Request headers |
| body | any | null | Request body |
| timeout | number | 30000 | Request timeout in milliseconds |
| followRedirects | boolean | true | Whether to follow redirects |
| maxRedirects | number | 5 | Maximum redirects to follow |
| validateStatus | boolean | true | Throw HTTPError on 4xx/5xx status |
| maxRetries | number | 0 | Number of retry attempts |
| retryDelay | number | 1000 | Base retry delay in milliseconds |
| retryStrategy | string | 'exponential' | Retry strategy: 'fixed', 'exponential', 'exponential_jitter' |
| responseType | string | 'auto' | Response type: 'auto', 'json', 'text', 'buffer', 'stream' |
| signal | AbortSignal | null | AbortSignal for cancellation |
| params | object | {} | URL query parameters |
| auth | object | null | Authentication { username, password } |
| baseURL | string | null | Base URL prepended to relative URLs |
| rateLimit | object | null | Rate limiting configuration |
| retry | object | null | Pattern-based retry configuration |
Class: StealthClient
The main client class for making HTTP requests.
new StealthClient([options])
optionsDefault Options for all requests
Creates a new StealthClient instance with default options.
const client = new StealthClient({
baseURL: 'https://api.example.com',
timeout: 10000,
headers: { 'Authorization': 'Bearer token' }
});client.get(url[, options])
urlRequest URLoptionsRequest Options- Returns:
Promise<StealthResponse>
client.post(url[, body[, options]])
urlRequest URLbodyRequest body (automatically serialized if object)optionsRequest Options- Returns:
Promise<StealthResponse>
client.put(url[, body[, options]])
client.patch(url[, body[, options]])
client.delete(url[, options])
client.head(url[, options])
client.use(middleware)
middlewareMiddleware object or function- Returns:
StealthClient(for chaining)
Add middleware to the request pipeline.
client.getRateLimiter()
- Returns:
RateLimiter | null
Get the rate limiter instance.
client.setRateLimit(config)
configRate limit configuration object ornullto disable- Returns:
void
Update or disable rate limiting.
Class: StealthResponse
An HTTP response object.
response.ok
boolean
true if status is 200-299.
response.status
number
HTTP status code.
response.statusText
string
HTTP status message.
response.headers
Headers
Response headers.
response.url
string
Final URL after redirects.
response.json()
- Returns:
Promise<any>
Parse response body as JSON.
response.text()
- Returns:
Promise<string>
Get response body as string.
response.buffer()
- Returns:
Promise<Buffer>
Get response body as Buffer.
response.stream()
- Returns:
Readable
Get response body as Node.js Readable stream.
Class: Headers
HTTP headers with case-insensitive key access.
new Headers([init])
initOptional object, Headers instance, or entries array
import { Headers } from 'stealth-io';
const headers = new Headers({
'Content-Type': 'application/json',
'X-Custom-Header': 'value'
});
headers.get('content-type');
headers.set('Authorization', 'Bearer token');
headers.has('x-custom-header');
headers.delete('X-Custom-Header');Class: StealthIOError
Base error class for all Stealth-IO errors.
Class: TimeoutError
Thrown when a request times out.
error.timeout- The timeout value in milliseconds
Class: AbortError
Thrown when a request is aborted via AbortSignal.
Class: NetworkError
Thrown for network-level errors (DNS, connection refused, etc).
error.code- System error code (e.g.,'ECONNREFUSED')
Class: HTTPError
Thrown for HTTP error status codes when validateStatus is enabled.
error.status- HTTP status codeerror.statusText- HTTP status messageerror.response- The StealthResponse object
Class: RetryError
Thrown when all retry attempts are exhausted.
error.attempts- Number of attempts madeerror.lastError- The last error encountered
Class: RateLimitError
Thrown when rate limit is exceeded and queue is disabled.
error.retryAfter- Suggested wait time in millisecondserror.queueSize- Current queue size
import { createClient, RateLimitError } from 'stealth-io';
const client = createClient({
rateLimit: { maxRequests: 2, perMs: 1000, queue: false }
});
try {
await client.get('/endpoint');
} catch (error) {
if (error instanceof RateLimitError) {
console.log(`Rate limited. Retry after ${error.retryAfter}ms`);
}
}Middleware
Built-in Middleware
import {
createLoggingMiddleware,
createAuthMiddleware,
createCacheMiddleware,
createTimingMiddleware,
createHeadersMiddleware,
createErrorMiddleware
} from 'stealth-io';Logging Middleware
client.use(createLoggingMiddleware({
logger: console.log,
logRequest: true,
logResponse: true
}));Auth Middleware
client.use(createAuthMiddleware({
type: 'bearer',
token: 'your-token'
}));
client.use(createAuthMiddleware({
type: 'basic',
username: 'user',
password: 'pass'
}));Cache Middleware
client.use(createCacheMiddleware({
ttl: 60000,
maxSize: 100
}));Timing Middleware
client.use(createTimingMiddleware({
onTiming: (timing) => console.log(`Request took ${timing.duration}ms`)
}));Custom Middleware
client.use({
name: 'custom-middleware',
onRequest(config) {
config.headers.set('X-Request-ID', generateUUID());
return config;
},
onResponse(response, config) {
console.log(`Response received: ${response.status}`);
return response;
},
onError(error, config) {
console.error(`Request failed: ${error.message}`);
return null;
}
});Testing
npm test
npm run test:coverageArchitecture
stealth-io/
src/
core/ # Constants, symbols, and error classes
utils/ # Utility functions (headers, query, url, streams)
types/ # Request and response type definitions
http/ # Transport, abort, retry, rate-limiter, pattern-retry
middleware/ # Middleware pipeline and built-in middleware
client/ # Main StealthClient class
index.js # Main entry point
test/ # Jest testsTeam
|
|
| ------------------------------------------------------------------------------------ |
| channdev |
Lead Author
Contributing
See CONTRIBUTING.md for details on how to contribute to this project.
