pingpong-fetch
v1.3.0
Published
Universal HTTP client for Node.js and browsers with automatic environment detection
Maintainers
Readme
🏓 pingpong-fetch
Universal HTTP client for Node.js and browsers with automatic environment detection
A lightweight, universal HTTP client that automatically detects your environment and uses the optimal implementation:
- Node.js: Uses
undicifor high performance - Browser: Uses native
fetch()API
✨ Features
- 🌐 Universal - Works seamlessly in Node.js and browsers
- ⚡ Fast - Uses
undiciin Node.js, nativefetchin browsers - 🔄 Zero Config - Automatic environment detection
- 📦 Small - Minimal dependencies (~5-6KB gzipped), tree-shakeable, optimized
- 🎯 Type-Safe - Full TypeScript support with type inference
- � Query Parameters - Built-in query param support at client and request level (v1.2.0)
- 📋 Case-Insensitive Headers - RFC 2616 compliant header access (v1.2.0)- 🍪 Credentials Control - Control cookie sending with
credentialsoption (Browser)- 🔁 Retry Logic - Built-in retry with exponential backoff - 📄 Response Methods - Chainable methods:
.json(),.text(),.raw(),.ok(),.isError(),.getHeader() - 📡 Streaming - Support for streaming large files and responses
- 🛠️ Convenience Methods -
.get(),.post(),.put(),.delete(),.patch(),.head(),.options() - ⏱️ Timeout Support - Configurable timeouts at client and request level
- 🔀 Proxy Support - HTTP proxy support (Node.js)
- 🚫 Request Cancellation - AbortSignal support for cancelling requests
- 🔁 Redirect Handling - Automatic redirect following with configurable limits
📦 Installation
npm install pingpong-fetch🚀 Quick Start
import { HttpClient } from 'pingpong-fetch';
// Create a client with default options and params
const client = new HttpClient({
timeout: 5000,
params: { api_key: 'secret' } // Default params for all requests
});
// Simple GET request with query parameters
const response = await client.get('https://api.mockly.codes/users', {
params: { page: 1, limit: 10 } // Merged with client params
});
console.log(response.status);
console.log(response.text()); // Get body as text
// Case-insensitive header access
console.log(response.getHeader('content-type')); // Works with any casing
console.log(response.getHeader('Content-Type')); // Same result
// Parse JSON from response
const user = await client.get('https://api.mockly.codes/users/1').then(r => r.json());
console.log(user.name);
// POST with JSON (automatically stringified)
const createResponse = await client.post('https://api.mockly.codes/users', {
name: 'John Doe',
email: '[email protected]'
});
// Chain response methods
const newUser = await client.post('https://api.mockly.codes/users', {
name: 'Jane Doe',
email: '[email protected]'
}).then(r => r.json());
console.log(newUser.id);📊 Performance
- Bundle Size: ~5-6KB gzipped (25% smaller than axios)
- Throughput: ~4,250 req/s average
- Latency: ~0.24ms average response time
- Build Time: Optimized with stripped source maps and comments
📚 Table of Contents
- Features
- Performance
- Installation
- Quick Start
- Examples
- API Reference
- Environment Detection
- Implementation Guide
- License
🤔 Common Questions & Issues?
Check out the Implementation Guide for:
- ✅ Convenience methods availability in Node.js
- ✅ baseURL with relative paths
- ✅ Response object structure and chainable methods
- ✅ FormData upload with complete examples
- ✅ Node.js vs Browser differences
- ✅ Common issues and solutions
- ✅ Best practices
This document addresses all known issues and misconceptions about the library.
📚 Need Help?
New to the library? Check the Documentation Index - it helps you find exactly what you need!
| Need | Go To | |------|-------| | Quick code examples | QUICK_REFERENCE.md | | Detailed explanations | IMPLEMENTATION_GUIDE.md | | How issues were fixed | ISSUES_RESOLUTION.md | | Working examples | examples/ | | Navigate all docs | DOCS_INDEX.md |
📖 Examples
Check out the examples directory for complete working examples of all features:
Node.js Examples
- Basic GET - Simple GET request
- POST with JSON - Send JSON data
- Headers & Auth - Authentication and custom headers
- Response Methods - .json(), .text(), .ok(), etc.
- Convenience Methods - .get(), .post(), .put(), .delete()
- FormData Upload - File uploads with FormData
- Error Handling - Handle errors gracefully
- Retry Logic - Automatic retry with backoff
- Timeout - Configure timeouts
- Abort Signal - Cancel requests
- Proxy Support - Route through proxy
- Credentials - Cookie control (Browser)
Browser Examples
- Browser Usage - Interactive browser example
- Environment Detection - Automatic detection
See examples/README.md for complete example documentation.
📖 API Reference
HttpClient
The main HTTP client class.
const client = new HttpClient(options?: HttpClientOptions);Constructor Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| baseURL | string | undefined | Base URL prepended to relative URLs |
| params | Record<string, any> | undefined | Default query parameters for all requests (v1.2.0) |
| timeout | number | undefined | Default request timeout in milliseconds |
| headers | HttpHeaders | {} | Default headers for all requests |
| proxy | string | undefined | HTTP proxy URL (Node.js only) |
| followRedirects | boolean | true | Automatically follow redirects |
| maxRedirects | number | 10 | Maximum number of redirects to follow |
| credentials | 'omit' \| 'same-origin' \| 'include' | undefined | Controls cookie sending behavior (Browser only) |
| retries | number | 0 | Default number of retry attempts |
| retryDelay | number | 1000 | Default delay between retries (ms, uses exponential backoff) |
| retryCondition | (response: HttpResponse) => boolean | See below | Function to determine if request should be retried |
Default Retry Condition:
(response) => response.status === 0 || response.status >= 500 || response.status === 429Retries on network errors (status 0), server errors (5xx), and rate limiting (429).
Methods
Core Methods
send(request: HttpRequest, options?: RequestOptions): Promise<HttpResponse>
Send a custom HTTP request.
const response = await client.send({
method: 'GET',
url: 'https://api.mockly.codes/data',
headers: { 'Authorization': 'Bearer token' }
});sendStream(request: HttpRequest, options?: RequestOptions): Promise<StreamResponse>
Send a request and return a stream instead of buffering the response.
const { stream, status, headers } = await client.sendStream({
method: 'GET',
url: 'https://api.mockly.codes/large-file'
});Convenience Methods
All convenience methods accept an optional RequestOptions parameter to override client defaults.
GET Requests
// GET request returning response with chainable methods
client.get(url: string, options?: RequestOptions): Promise<HttpResponse>
// GET returning stream
client.getStream(url: string, options?: RequestOptions): Promise<StreamResponse>POST Requests
// POST request (data automatically stringified if object)
client.post(url: string, data?: any, options?: RequestOptions): Promise<HttpResponse>PUT Requests
// PUT request
client.put(url: string, data?: any, options?: RequestOptions): Promise<HttpResponse>PATCH Requests
// PATCH request
client.patch(url: string, data?: any, options?: RequestOptions): Promise<HttpResponse>DELETE, HEAD, OPTIONS
client.delete(url: string, options?: RequestOptions): Promise<HttpResponse>
client.head(url: string, options?: RequestOptions): Promise<HttpResponse>
client.options(url: string, options?: RequestOptions): Promise<HttpResponse>Response Methods
All convenience methods return an HttpResponse with chainable helper methods:
json<T>(): T
Parse response body as JSON.
const user = await client.get('https://api.mockly.codes/users/1').then(r => r.json());text(): string
Get response body as text.
const html = await client.get('https://example.com').then(r => r.text());raw(): HttpResponseBase
Get the raw response object (without methods).
const response = await client.get('/data');
const raw = response.raw();getHeaders(): HttpHeaders
Get response headers.
const contentType = await client.get('/data').then(r => r.getHeaders()['content-type']);ok(): boolean
Check if status is 2xx (200-299).
const response = await client.get('/data');
if (response.ok()) {
console.log('Success!');
}redirected(): boolean
Check if response was a redirect (3xx).
const response = await client.get('/redirect');
console.log(response.redirected()); // trueisError(): boolean
Check if status is 4xx or 5xx.
const response = await client.get('/data');
if (response.isError()) {
console.error(response.json());
}getHeader(name: string): string | undefined
Get response header value (case-insensitive, RFC 2616 compliant). New in v1.2.0
const response = await client.get('/data');
// All of these return the same value
response.getHeader('content-type'); // 'application/json'
response.getHeader('Content-Type'); // 'application/json'
response.getHeader('CONTENT-TYPE'); // 'application/json'create(options: HttpClientOptions): HttpClient
Create a new client instance with merged options.
const apiClient = client.create({ baseURL: 'https://api.mockly.codes' });Types
HttpRequest
interface HttpRequest {
method: HttpMethod; // 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'
url: string; // Request URL
headers?: HttpHeaders; // Request headers
body?: string | Uint8Array | Blob | FormData; // Request body
timeout?: number; // Request timeout (ms)
signal?: AbortSignal; // Abort signal for cancellation
proxy?: string; // Proxy URL (Node.js only)
followRedirects?: boolean; // Follow redirects
maxRedirects?: number; // Max redirects to follow
credentials?: 'omit' | 'same-origin' | 'include'; // Cookie behavior (Browser)
retries?: number; // Retry attempts
retryDelay?: number; // Retry delay (ms)
validateStatus?: (status: number) => boolean; // Status validation
}HttpResponse
interface HttpResponse {
status: number; // HTTP status code
statusText: string; // HTTP status text
headers: HttpHeaders; // Response headers
body: string; // Response body (as string)
time: number; // Request duration (ms)
size: number; // Response size (bytes)
// Chainable methods
raw(): HttpResponseBase; // Get raw response
json<T>(): T; // Parse as JSON
text(): string; // Get body as text
getHeaders(): HttpHeaders; // Get headers
ok(): boolean; // Check if 2xx
redirected(): boolean; // Check if 3xx
isError(): boolean; // Check if 4xx/5xx
}StreamResponse
interface StreamResponse {
status: number; // HTTP status code
statusText: string; // HTTP status text
headers: HttpHeaders; // Response headers
stream: ReadableStream<Uint8Array> | NodeJS.ReadableStream; // Response stream
time: number; // Request duration (ms)
}RequestOptions
All options from HttpClientOptions can be overridden per-request via RequestOptions:
interface RequestOptions {
params?: Record<string, any>; // Query parameters (merged with client params)
timeout?: number;
headers?: HttpHeaders;
signal?: AbortSignal;
proxy?: string; // Node.js only
followRedirects?: boolean;
maxRedirects?: number;
credentials?: 'omit' | 'same-origin' | 'include'; // Cookie behavior (Browser)
retries?: number;
retryDelay?: number;
retryCondition?: (response: HttpResponse) => boolean;
}💡 Examples
Basic Usage
import { HttpClient } from 'pingpong-fetch';
const client = new HttpClient();
// GET request with chainable response methods
const response = await client.get('https://api.mockly.codes/users');
console.log(response.status); // 200
console.log(response.text()); // Response body as string
console.log(response.time); // Request duration in ms
console.log(response.size); // Response size in bytes
console.log(response.ok()); // true (status 2xx)
console.log(response.getHeaders()); // Response headersJSON Parsing
import { HttpClient } from 'pingpong-fetch';
const client = new HttpClient();
interface User {
id: number;
name: string;
email: string;
}
// GET and parse JSON using chainable method
const user = await client.get('https://api.mockly.codes/users/1').then(r => r.json<User>());
console.log(user.name); // Already parsed!
// POST with automatic JSON parsing
const newUser = await client.post('https://api.mockly.codes/users', {
name: 'John Doe',
email: '[email protected]'
}).then(r => r.json<User>());
console.log(newUser.id); // Already parsed!
// Or use async/await
const response = await client.get('https://api.mockly.codes/users/1');
const userData = response.json<User>();
console.log(userData.name);Retry Logic
import { HttpClient } from 'pingpong-fetch';
// Configure retry at client level
const client = new HttpClient({
retries: 3, // Retry up to 3 times
retryDelay: 1000, // Initial delay: 1 second (exponential backoff)
retryCondition: (response) => {
// Retry on server errors or rate limits
return response.status >= 500 || response.status === 429;
}
});
// All requests will automatically retry
const response = await client.get('https://api.mockly.codes/flaky-endpoint');
// Override retry for specific request
const response2 = await client.get('https://api.mockly.codes/data', {
retries: 5, // Retry 5 times for this request
retryDelay: 2000, // 2 second initial delay
retryCondition: (res) => res.status === 503 // Only retry on 503
});
// Disable retry for specific request
const response3 = await client.get('https://api.mockly.codes/data', {
retries: 0 // No retries
});Retry Behavior:
- Uses exponential backoff: delay × 2^attempt (1s, 2s, 4s, 8s...)
- Respects abort signals (won't retry if cancelled)
- Default condition: retries on status 0 (network error), 5xx (server error), or 429 (rate limit)
Streaming
import { HttpClient } from 'pingpong-fetch';
const client = new HttpClient();
// Stream large file download (Node.js)
const streamResponse = await client.getStream('https://example.com/large-file.zip');
// Node.js: ReadableStream
const stream = streamResponse.stream as NodeJS.ReadableStream;
const fs = require('fs');
const writeStream = fs.createWriteStream('downloaded-file.zip');
stream.pipe(writeStream);
// Browser: ReadableStream<Uint8Array>
const streamResponse2 = await client.getStream('https://example.com/large-file.zip');
const reader = streamResponse2.stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
// Process chunk
console.log('Received chunk:', value.length, 'bytes');
// Save to file, process data, etc.
}
// Stream POST request
const streamResponse3 = await client.sendStream({
method: 'POST',
url: 'https://api.mockly.codes/upload',
body: fileData
});Request Cancellation
import { HttpClient } from 'pingpong-fetch';
const client = new HttpClient();
const controller = new AbortController();
// Start request with abort signal
const requestPromise = client.get('https://api.mockly.codes/slow-endpoint', {
signal: controller.signal
});
// Cancel the request
controller.abort();
// Handle cancellation
try {
const response = await requestPromise;
} catch (error) {
// Request was cancelled
console.log('Request cancelled');
}
// React component example
import { useEffect, useState } from 'react';
function DataComponent() {
const [data, setData] = useState(null);
const client = new HttpClient();
useEffect(() => {
const controller = new AbortController();
async function fetchData() {
try {
const response = await client.get('https://api.mockly.codes/data', {
signal: controller.signal
});
setData(response.json());
} catch (error) {
if (!controller.signal.aborted) {
console.error('Failed to fetch:', error);
}
}
}
fetchData();
// Cancel request on unmount
return () => controller.abort();
}, []);
return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}Query Parameters
import { HttpClient } from 'pingpong-fetch';
// Client-level default params
const client = new HttpClient({
baseURL: 'https://api.example.com',
params: { api_key: 'secret', version: 'v1' } // Applied to all requests
});
// Request without params uses client defaults
const response1 = await client.get('/users');
// → https://api.example.com/users?api_key=secret&version=v1
// Request-level params are merged with client params
const response2 = await client.get('/users', {
params: { page: 1, limit: 10 } // Added to client params
});
// → https://api.example.com/users?api_key=secret&version=v1&page=1&limit=10
// Request params override client params
const response3 = await client.get('/users', {
params: { version: 'v2', page: 2 } // version=v2 overrides client's v1
});
// → https://api.example.com/users?api_key=secret&version=v2&page=2
// Array parameters (automatically repeated)
const response4 = await client.get('/products', {
params: {
tags: ['new', 'featured', 'bestseller'],
colors: ['red', 'blue']
}
});
// → /products?tags=new&tags=featured&tags=bestseller&colors=red&colors=blue
// No client, just request params
const simpleClient = new HttpClient();
const response5 = await simpleClient.get('https://api.example.com/users', {
params: { search: 'john', active: true }
});
// → https://api.example.com/users?search=john&active=trueBenefits:
- No manual URL construction
- Automatic encoding and escaping
- Clean separation of URL and params
- Request-level params override client defaults
- Array support built-in
Case-Insensitive Headers
import { HttpClient } from 'pingpong-fetch';
const client = new HttpClient();
const response = await client.get('https://api.example.com/data');
// Access headers with any casing (RFC 2616 compliant)
response.getHeader('content-type'); // 'application/json'
response.getHeader('Content-Type'); // 'application/json'
response.getHeader('CONTENT-TYPE'); // 'application/json'
response.getHeader('CoNtEnT-tYpE'); // 'application/json'
// All return the same value
const contentType = response.getHeader('content-type');
if (contentType?.includes('json')) {
const data = response.json();
}Benefits:
- RFC 2616 HTTP spec compliant
- No case-sensitivity bugs
- Works with any casing variation
- Clean, predictable API
Credentials and Cookie Control (Browser)
import { HttpClient } from 'pingpong-fetch';
// Control cookie sending behavior
const client = new HttpClient({
credentials: 'include' // Send cookies with all requests, even cross-origin
});
// Make authenticated requests with cookies
const response = await client.get('https://api.example.com/user/profile');
// Override per-request
const publicData = await client.get('https://api.example.com/public', {
credentials: 'omit' // Don't send cookies for this request
});
// Same-origin only (default browser behavior)
const sameOrigin = await client.get('https://api.example.com/data', {
credentials: 'same-origin' // Only send cookies for same-origin requests
});Credentials Options:
'omit'- Never send cookies'same-origin'- Send cookies only for same-origin requests (browser default)'include'- Always send cookies, even for CORS/cross-origin requests
Benefits:
- Control cookie behavior explicitly
- Follows Fetch API standard
- Works seamlessly with authentication flows
- Per-request override capability
Note: This option only works in browsers. Node.js requests should manage cookies manually using headers.
Base URL and Headers
import { HttpClient } from 'pingpong-fetch';
// Create client with base URL and default headers
const client = new HttpClient({
baseURL: 'https://api.mockly.codes',
headers: {
'Authorization': 'Bearer token',
'Content-Type': 'application/json'
},
timeout: 5000
});
// Relative URL (uses baseURL)
const users = await client.getJson('/users');
// Absolute URL (ignores baseURL)
const external = await client.get('https://other-api.com/data');
// Override headers for specific request
const response = await client.get('/users', {
headers: {
'Authorization': 'Bearer different-token'
}
});Proxy Support (Node.js)
import { HttpClient } from 'pingpong-fetch';
const client = new HttpClient({
proxy: 'http://proxy.example.com:8080'
});
// All requests go through proxy
const response = await client.get('https://api.mockly.codes/data');
// Override proxy for specific request
const response2 = await client.get('https://api.mockly.codes/data', {
proxy: 'http://other-proxy.com:8080'
});Redirect Handling
import { HttpClient } from 'pingpong-fetch';
const client = new HttpClient({
followRedirects: true, // Default: true
maxRedirects: 10 // Default: 10
});
// Automatically follows redirects
const response = await client.get('https://api.mockly.codes/redirect');
// Disable redirects for specific request
const response2 = await client.get('https://api.mockly.codes/redirect', {
followRedirects: false
});
// Limit redirects for specific request
const response3 = await client.get('https://api.mockly.codes/redirect', {
maxRedirects: 5
});TypeScript Support
import { HttpClient, HttpResponse } from 'pingpong-fetch';
interface User {
id: number;
name: string;
email: string;
}
interface ApiResponse<T> {
data: T;
status: 'success' | 'error';
message?: string;
}
const client = new HttpClient();
// Type-safe JSON parsing with chainable methods
const user = await client.get('https://api.mockly.codes/users/1').then(r => r.json<User>());
console.log(user.name); // TypeScript knows this is a string
// Type-safe response handling
const response: HttpResponse = await client.get('https://api.mockly.codes/users');
const users: ApiResponse<User[]> = response.json<ApiResponse<User[]>>();
users.data.forEach(user => {
console.log(user.email); // Fully typed!
});
// Check response status with type-safe methods
if (response.ok()) {
const data = response.json<User>();
console.log(data);
}Error Handling
import { HttpClient } from 'pingpong-fetch';
const client = new HttpClient();
try {
const response = await client.get('https://api.mockly.codes/data');
// Use chainable methods to check response
if (response.isError()) {
console.error('HTTP Error:', response.status, response.statusText);
const error = response.json();
console.error('Error details:', error);
} else if (response.ok()) {
const data = response.json();
console.log('Success!', data);
}
} catch (error) {
// Network errors, timeouts, etc.
console.error('Request failed:', error);
}
// Check for cancelled requests
const controller = new AbortController();
const response = await client.get('https://api.mockly.codes/data', {
signal: controller.signal
});
if (response.status === 0 && response.statusText === 'Cancelled') {
console.log('Request was cancelled');
}🌍 Environment Detection
The library automatically detects the runtime environment and uses the optimal implementation:
- Node.js: Uses
undicifor high performance - Browser: Uses native
fetch()API
Automatic Detection (Default)
import { HttpClient } from 'pingpong-fetch';
// Automatically uses the right implementationExplicit Entry Points
For bundlers like webpack, you can use specific entry points:
Browser-only (recommended for browser bundles):
import { HttpClient } from 'pingpong-fetch/browser';
// Only includes browser code, no Node.js dependenciesNode.js-only:
import { HttpClient } from 'pingpong-fetch/node';
// Only includes Node.js code with undiciBundler Configuration
For browser builds, the library automatically uses the browser entry point via the browser field in package.json. Most modern bundlers (Vite, Webpack, Rollup) will automatically pick this up.
Vite (automatic, no config needed):
import { HttpClient } from 'pingpong-fetch';
// Automatically uses browser entry pointWebpack (optional explicit alias):
resolve: {
alias: {
'pingpong-fetch': 'pingpong-fetch/browser'
}
}📋 Features by Environment
| Feature | Node.js | Browser | |---------|---------|---------| | HTTP Methods | ✅ | ✅ | | Custom Headers | ✅ | ✅ | | Request Body | ✅ | ✅ | | Timeout | ✅ | ✅ | | Redirects | ✅ | ✅ | | Proxy Support | ✅ | ⚠️ (logs warning) | | Binary Data | ✅ | ✅ | | AbortSignal | ✅ | ✅ | | Streaming | ✅ | ✅ | | Retry Logic | ✅ | ✅ | | JSON Parsing | ✅ | ✅ | | Credentials | ❌ | ✅ |
🤝 Contributing
Contributions are welcome! Please read our Contributing Guide for details.
📄 License
MIT © 0xdps
🔗 Related Packages
pingpong-cli- Command-line HTTP clientpingpong-core- Core utilities and types- PingPong Extension - Browser extension
💬 Support
For questions, issues, or feature requests, please open an issue or contact [email protected].
