npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

pingpong-fetch

v1.3.0

Published

Universal HTTP client for Node.js and browsers with automatic environment detection

Readme

🏓 pingpong-fetch

Universal HTTP client for Node.js and browsers with automatic environment detection

npm version License: MIT

A lightweight, universal HTTP client that automatically detects your environment and uses the optimal implementation:

  • Node.js: Uses undici for high performance
  • Browser: Uses native fetch() API

✨ Features

  • 🌐 Universal - Works seamlessly in Node.js and browsers
  • Fast - Uses undici in Node.js, native fetch in 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 credentials option (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

🤔 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

Browser Examples

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 === 429

Retries 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()); // true
isError(): 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 headers

JSON 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=true

Benefits:

  • 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 undici for high performance
  • Browser: Uses native fetch() API

Automatic Detection (Default)

import { HttpClient } from 'pingpong-fetch';
// Automatically uses the right implementation

Explicit 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 dependencies

Node.js-only:

import { HttpClient } from 'pingpong-fetch/node';
// Only includes Node.js code with undici

Bundler 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 point

Webpack (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

💬 Support

For questions, issues, or feature requests, please open an issue or contact [email protected].