# @push.rocks/webrequest

> Modern, fetch-compatible web request library with intelligent HTTP caching, retry strategies, and fault tolerance.

Latest version **4.3.1** (published 2026-08-23) · MIT license · 0 weekly downloads

## Install

```sh
npm install @push.rocks/webrequest
pnpm add @push.rocks/webrequest
yarn add @push.rocks/webrequest
bun add @push.rocks/webrequest
```

## Health

**Score 70/100 (B)** — status: active.

Positive: has types; esm support; no vulnerabilities; recently updated; high maintenance score; high quality score.

Warnings: low downloads.

## Facts

| | |
|---|---|
| Version | 4.3.1 |
| Published | 2026-08-23 |
| First published | 2023-07-10 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | bundled |
| Module format | ESM + CommonJS |
| Dependencies | 5 |
| Unpacked size | 2.2 MB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Author | Lossless GmbH |
| Maintainers | lossless |
| Keywords | webrequest, HTTP, secure, browsers, caching, fault tolerance, json, abort, timeout, multi-endpoint, fetch API |

## Links

- npm: https://www.npmjs.com/package/@push.rocks/webrequest
- Repository: https://code.foss.global/push.rocks/webrequest
- Homepage: https://code.foss.global/push.rocks/webrequest#readme
- Issues: https://code.foss.global/push.rocks/webrequest/issues
- npm.io page: https://npm.io/package/@push.rocks/webrequest

## Dependencies (5)

- [@push.rocks/smartenv](https://npm.io/package/@push.rocks/smartenv.md) ^6.1.0
- [@push.rocks/webstore](https://npm.io/package/@push.rocks/webstore.md) ^2.2.0
- [@push.rocks/smartjson](https://npm.io/package/@push.rocks/smartjson.md) ^6.0.1
- [@push.rocks/smartdelay](https://npm.io/package/@push.rocks/smartdelay.md) ^3.1.0
- [@push.rocks/smartpromise](https://npm.io/package/@push.rocks/smartpromise.md) ^4.2.4

## Alternatives

- [@mapbox/jsonlint-lines-primitives](https://npm.io/package/@mapbox/jsonlint-lines-primitives.md) — 5.3M weekly downloads
- [reftools](https://npm.io/package/reftools.md) — 3.5M weekly downloads
- [@hey-api/openapi-ts](https://npm.io/package/@hey-api/openapi-ts.md) — 3.5M weekly downloads
- [@mapbox/geojson-rewind](https://npm.io/package/@mapbox/geojson-rewind.md) — 2.4M weekly downloads
- [turbo-stream](https://npm.io/package/turbo-stream.md) — 1.7M weekly downloads

## Recent versions

- 4.3.1 (latest) — 2026-08-23
- 4.3.0 — 2026-08-15
- 4.2.0 — 2026-07-29
- 4.1.1 — 2026-07-29
- 4.1.0 — 2026-07-29
- 4.0.6 — 2026-07-29
- 4.0.5 — 2026-03-02
- 4.0.1 — 2025-10-20
- 3.0.37 — 2024-04-17
- 3.0.36 — 2024-04-17
- 3.0.35 — 2024-03-03
- 3.0.34 — 2023-10-20
- 3.0.33 — 2023-08-27
- 3.0.32 — 2023-07-27
- 3.0.29 — 2023-07-10

## README

# @push.rocks/webrequest

Modern, fetch-compatible web request library with intelligent HTTP caching, retry strategies, and advanced fault tolerance. Works seamlessly in browsers, Node.js, Deno, and Bun.

## Issue Reporting and Security

For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.

## Features

- 🌐 **Fetch-Compatible API** — Drop-in replacement for native `fetch()` with enhanced features
- 💾 **Intelligent HTTP Caching** — Respects `Cache-Control`, `ETag`, `Last-Modified`, and `Expires` headers (RFC 7234)
- 🔄 **Multiple Cache Strategies** — network-first, cache-first, stale-while-revalidate, network-only, cache-only
- 🔁 **Advanced Retry System** — Configurable retry with exponential, linear, or constant backoff
- 🎯 **Request/Response Interceptors** — Middleware pattern for transforming requests and responses
- 🚫 **Request Deduplication** — Automatically deduplicate simultaneous identical requests
- 📘 **TypeScript Generics** — Type-safe response parsing with `webrequest.getJson<T>()`
- 🛡️ **Multi-Endpoint Fallback** — Fault tolerance via fallback URLs with retry strategies
- ⏱️ **Timeout Support** — Configurable request timeouts with AbortController
- 🌍 **Cross-Runtime** — Works in browsers, Node.js, Deno, and Bun

## Installation

```bash
pnpm install @push.rocks/webrequest
# or
npm install @push.rocks/webrequest
```

This package requires a modern JavaScript environment with ESM and TypeScript support.

## Quick Start

### Basic Fetch-Compatible Usage

```typescript
import { webrequest } from '@push.rocks/webrequest';

// Use exactly like fetch()
const response = await webrequest('https://api.example.com/data');
const data = await response.json();

// With options (fetch-compatible + enhanced)
const response = await webrequest('https://api.example.com/data', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ key: 'value' }),
  timeout: 30000,
  retry: true,
});
```

### JSON Convenience Methods

```typescript
import { webrequest } from '@push.rocks/webrequest';

// GET JSON with type safety
interface User {
  id: number;
  name: string;
  email: string;
}

const user = await webrequest.getJson<User>('https://api.example.com/user/1');
// user is typed as User

// POST JSON
const result = await webrequest.postJson('https://api.example.com/users', {
  name: 'John Doe',
  email: 'john@example.com',
});

// PUT and DELETE
await webrequest.putJson(url, data);
await webrequest.deleteJson(url);
```

### Cache-Origin Response Metadata

Use the metadata-returning methods when a caller must distinguish a current
network response from a payload supplied by Webrequest's HTTP cache:

```typescript
const result = await webrequest.postJsonWithMetadata<ApiResponse>(
  'https://api.example.com/typedrequest',
  requestPayload,
  {
    cacheStrategy: 'cache-first',
    cacheKey: 'typedrequest:example',
  },
);

console.log(result.data);
console.log(result.metadata.fromCache);
console.log(result.metadata.revalidated);
```

`metadata` is frozen and returned separately from HTTP headers, so a server
cannot spoof cache origin. `fromCache` means that the foreground response body
came from Webrequest's persistent cache. `revalidated` means that Webrequest
completed a conditional revalidation for this response. When the origin answers
`304 Not Modified`, Webrequest returns the cached response body and status while
the metadata reports both flags as `true`. A changed network response obtained
during revalidation reports `fromCache: false` and `revalidated: true`.
Network-only and multi-endpoint fallback responses report both flags as
`false`. Retried requests preserve the final cache strategy result, so a retry
may still finish from cache or through conditional revalidation.

Raw response callers can use `webrequest.requestWithMetadata()` or
`client.requestWithMetadata()`. JSON callers can use
`getJsonWithMetadata()`, `postJsonWithMetadata()`, `putJsonWithMetadata()`, or
`deleteJsonWithMetadata()`. JSON results also include the native `response` for
status and header inspection; its body has already been consumed. Existing
`webrequest()`, `request()`, and JSON methods keep their original return values
and behavior.

## Cache Strategies

### Network-First (Default)

Always fetch from network, fall back to cache on failure. Respects HTTP caching headers.

```typescript
const data = await webrequest.getJson('https://api.example.com/data', {
  cacheStrategy: 'network-first',
});
```

### Cache-First

Check cache first, only fetch from network if not cached or stale.

```typescript
const data = await webrequest.getJson('https://api.example.com/data', {
  cacheStrategy: 'cache-first',
  cacheMaxAge: 60000, // 60 seconds; use 0 for immediate staleness
  cacheMaxEntries: 256,
});
```

Persistent caches are capped at 256 entries by default. Set
`cacheMaxEntries` to another positive integer to change the cap. Webrequest
removes expired or malformed entries first, then evicts the oldest stored
responses by timestamp. Concurrent writes share one coalesced pruning loop;
browser tabs sharing the same IndexedDB database converge on the same bound on
subsequent writes.

### Stale-While-Revalidate

Return cached data immediately, update in background.

```typescript
const data = await webrequest.getJson('https://api.example.com/data', {
  cacheStrategy: 'stale-while-revalidate',
});
```

### Network-Only and Cache-Only

```typescript
// Always fetch from network, never cache
const fresh = await webrequest.getJson(url, {
  cacheStrategy: 'network-only',
});

// Only use cache, never fetch from network
const cached = await webrequest.getJson(url, {
  cacheStrategy: 'cache-only',
});
```

### HTTP Header-Based Caching

The library automatically respects HTTP caching headers:

```typescript
// Server returns: Cache-Control: max-age=0, no-cache; ETag: "abc123"
const response = await webrequest('https://api.example.com/data', {
  cacheStrategy: 'cache-first',
});

// A stale cache entry with validators sends:
// If-None-Match: "abc123"
// Server returns 304 Not Modified; Webrequest returns the cached response
```

### Custom Cache Keys

```typescript
const response = await webrequest('https://api.example.com/search?q=test', {
  cacheStrategy: 'cache-first',
  cacheKey: (request) => {
    const url = new URL(request.url);
    return `search:${url.searchParams.get('q')}`;
  },
});
```

## Retry Strategies

### Basic Retry

```typescript
const response = await webrequest('https://api.example.com/data', {
  retry: true, // Uses defaults: 3 attempts, exponential backoff
});
```

### Advanced Retry Configuration

```typescript
const response = await webrequest('https://api.example.com/data', {
  retry: {
    maxAttempts: 5,
    backoff: 'exponential', // or 'linear', 'constant'
    initialDelay: 1000,     // 1 second
    maxDelay: 30000,        // 30 seconds
    retryOn: [408, 429, 500, 502, 503, 504],
    onRetry: (attempt, error, nextDelay) => {
      console.log(`Retry attempt ${attempt}, waiting ${nextDelay}ms`);
    },
  },
});
```

### Multi-Endpoint Fallback

When the primary endpoint fails, automatically try fallback URLs:

```typescript
const response = await webrequest('https://api1.example.com/data', {
  fallbackUrls: [
    'https://api2.example.com/data',
    'https://api3.example.com/data',
  ],
  retry: {
    maxAttempts: 3,
    backoff: 'exponential',
  },
});
```

Each URL is tried with the configured retry strategy. If all attempts for a URL fail with server errors, the next fallback URL is tried.

## Timeouts and cancellation

Raw `webrequest()` / `client.request()` calls return an unmodified native
`Response`. Their timeout bounds each network attempt until response headers
arrive when persistent caching is bypassed. Cache-using calls apply the same
timeout to the foreground cache operation, including response-body
serialization, before returning the caller-owned response. The caller retains
normal `Response` ownership for subsequent body streaming, cloning, and Cache
API use.

The JSON convenience methods (`getJson`, `postJson`, `putJson`, and
`deleteJson`) also bound response-body consumption. With retries disabled
(the default), one deadline covers the complete request and JSON body. When
Webrequest retries are enabled, the timeout remains per attempt and the final
body receives the same timeout budget.

A caller-provided `AbortSignal` cancels the active fetch or body read and also
interrupts retry backoff, persistent-cache reads and writes, cache response
cloning, and pruning. Discarded retry, fallback, and unsuccessful JSON
responses have their bodies canceled before the operation continues. Abort or
deadline failures never fall back to a stale cached response.

Use the exported `isWebrequestTimeoutError(error)` predicate when a consumer
needs to distinguish Webrequest-owned deadline failures without depending on
error message text.

Stale-while-revalidate refreshes are coalesced per cache key and use the
request's configured timeout as an independent whole-refresh deadline (60
seconds by default), including response-body caching. Call `await
client.close()` for clients created with `webrequest.createClient()` when their
lifecycle ends; this aborts owned background refreshes and closes IndexedDB.

Use `cacheStrategy: 'network-only'` when a timed request must neither access
persistent cache nor fall back to a cached response.

## Request/Response Interceptors

### Global Interceptors

```typescript
import { webrequest } from '@push.rocks/webrequest';

// Add authentication to all requests
webrequest.addRequestInterceptor((request) => {
  const headers = new Headers(request.headers);
  headers.set('Authorization', `Bearer ${getToken()}`);
  return new Request(request, { headers });
});

// Log all responses
webrequest.addResponseInterceptor((response) => {
  console.log(`${response.status} ${response.url}`);
  return response;
});

// Handle errors globally
webrequest.addErrorInterceptor((error) => {
  console.error('Request failed:', error);
  return error;
});

// Clear all interceptors when needed
webrequest.clearInterceptors();
```

### Per-Request Interceptors

```typescript
const response = await webrequest('https://api.example.com/data', {
  interceptors: {
    request: [(req) => {
      console.log('Sending:', req.url);
      return req;
    }],
    response: [(res) => {
      console.log('Received:', res.status);
      return res;
    }],
  },
});
```

## Request Deduplication

Automatically prevent duplicate simultaneous requests:

```typescript
// Only one actual network request is made
const [res1, res2, res3] = await Promise.all([
  webrequest('https://api.example.com/data', { deduplicate: true }),
  webrequest('https://api.example.com/data', { deduplicate: true }),
  webrequest('https://api.example.com/data', { deduplicate: true }),
]);

// All three get the same response (cloned)
```

Deduplication works for GET and HEAD requests by matching URL + method. Non-GET/HEAD requests are not deduplicated since they may have different bodies.

## WebrequestClient

For more control, use `WebrequestClient` to set default options that apply to all requests made through that client:

```typescript
import { WebrequestClient } from '@push.rocks/webrequest';

const apiClient = new WebrequestClient({
  logging: true,
  timeout: 30000,
  cacheStrategy: 'network-first',
  retry: {
    maxAttempts: 3,
    backoff: 'exponential',
  },
});

// Add global interceptors to this client
apiClient.addRequestInterceptor((request) => {
  const headers = new Headers(request.headers);
  headers.set('X-API-Key', 'my-key');
  return new Request(request, { headers });
});

// All requests through this client use the configured defaults
const data = await apiClient.getJson('https://api.example.com/data');

// Standard fetch-compatible API
const response = await apiClient.request('https://api.example.com/data');

// Create a client from the webrequest function
const client = webrequest.createClient({ timeout: 5000 });
```

## Advanced Features

### Timeout

```typescript
const response = await webrequest('https://api.example.com/data', {
  timeout: 5000, // 5 seconds — throws Error on timeout
});
```

For this raw response API, the timeout covers the network attempt until
response headers arrive when persistent caching is bypassed, and covers the
foreground cache operation when caching is enabled. Use a JSON convenience
method when the same option must also bound caller-owned body consumption.
Select `cacheStrategy: 'network-only'` when a request must fail on network
timeout instead of using a configured cache fallback.

### Cache Management

```typescript
// Clear all cached responses
await webrequest.clearCache();

// Delete one custom cache entry from an owned client
const client = webrequest.createClient();
await client.request('https://api.example.com/user', {
  cacheStrategy: 'cache-first',
  cacheKey: 'current-user',
});
await client.deleteCache('current-user');
await client.close();
```

## API Reference

### Main Function

```typescript
webrequest(input: string | Request | URL, options?: IWebrequestOptions): Promise<Response>
webrequest.requestWithMetadata(input: string | Request | URL, options?: IWebrequestOptions): Promise<IWebrequestResponseResult>
```

### Convenience Methods

```typescript
webrequest.getJson<T>(url: string, options?: IWebrequestOptions): Promise<T>
webrequest.postJson<T>(url: string, body: any, options?: IWebrequestOptions): Promise<T>
webrequest.putJson<T>(url: string, body: any, options?: IWebrequestOptions): Promise<T>
webrequest.deleteJson<T>(url: string, options?: IWebrequestOptions): Promise<T>
webrequest.getJsonWithMetadata<T>(url: string, options?: IWebrequestOptions): Promise<IWebrequestJsonResult<T>>
webrequest.postJsonWithMetadata<T>(url: string, body: any, options?: IWebrequestOptions): Promise<IWebrequestJsonResult<T>>
webrequest.putJsonWithMetadata<T>(url: string, body: any, options?: IWebrequestOptions): Promise<IWebrequestJsonResult<T>>
webrequest.deleteJsonWithMetadata<T>(url: string, options?: IWebrequestOptions): Promise<IWebrequestJsonResult<T>>
```

### Global Methods

```typescript
webrequest.addRequestInterceptor(interceptor: TRequestInterceptor): void
webrequest.addResponseInterceptor(interceptor: TResponseInterceptor): void
webrequest.addErrorInterceptor(interceptor: TErrorInterceptor): void
webrequest.clearInterceptors(): void
webrequest.clearCache(): Promise<void>
webrequest.createClient(options?: Partial<IWebrequestOptions>): WebrequestClient
webrequest.getDefaultClient(): WebrequestClient
```

Created `WebrequestClient` instances expose `close(): Promise<void>` for
deterministic cache-resource cleanup and
`deleteCache(cacheKey: string): Promise<void>` for deleting one cache entry
without clearing unrelated cached responses.

### Options Interface

```typescript
interface IWebrequestOptions extends Omit<RequestInit, 'cache'> {
  // Standard fetch options
  method?: string;
  headers?: HeadersInit;
  body?: BodyInit;

  // Caching
  cache?: 'default' | 'no-store' | 'reload' | 'no-cache' | 'force-cache' | 'only-if-cached';
  cacheStrategy?: 'network-first' | 'cache-first' | 'stale-while-revalidate' | 'network-only' | 'cache-only';
  cacheMaxAge?: number;
  cacheMaxEntries?: number;
  cacheKey?: string | ((request: Request) => string);
  revalidate?: boolean;

  // Retry & Fault Tolerance
  retry?: boolean | IRetryOptions;
  fallbackUrls?: string[];
  timeout?: number;

  // Interceptors
  interceptors?: {
    request?: TRequestInterceptor[];
    response?: TResponseInterceptor[];
  };

  // Deduplication
  deduplicate?: boolean;

  // Logging
  logging?: boolean;
}
```

### Retry Options

```typescript
interface IRetryOptions {
  maxAttempts?: number;        // Default: 3
  backoff?: 'exponential' | 'linear' | 'constant'; // Default: 'exponential'
  initialDelay?: number;       // Default: 1000 (ms)
  maxDelay?: number;           // Default: 30000 (ms)
  retryOn?: number[] | ((response: Response, error?: Error) => boolean);
  onRetry?: (attempt: number, error: Error, nextDelay: number) => void;
}
```

## Examples

### Complete Example with All Features

```typescript
import { webrequest } from '@push.rocks/webrequest';

async function fetchUserData(userId: string) {
  interface User {
    id: string;
    name: string;
    email: string;
  }

  const user = await webrequest.getJson<User>(
    `https://api.example.com/users/${userId}`,
    {
      cacheStrategy: 'stale-while-revalidate',
      cacheMaxAge: 300000, // 5 minutes
      retry: {
        maxAttempts: 3,
        backoff: 'exponential',
        retryOn: [500, 502, 503, 504],
      },
      fallbackUrls: [
        `https://api-backup.example.com/users/${userId}`,
      ],
      timeout: 10000,
      deduplicate: true,
      interceptors: {
        request: [(req) => {
          console.log(`Fetching user ${userId}`);
          return req;
        }],
      },
    }
  );

  return user;
}
```

### Building a Typed API Client

```typescript
import { WebrequestClient } from '@push.rocks/webrequest';

interface User {
  id: string;
  name: string;
  email: string;
}

interface CreateUserData {
  name: string;
  email: string;
}

class ApiClient {
  private client: WebrequestClient;

  constructor(private baseUrl: string, private apiKey: string) {
    this.client = new WebrequestClient({
      timeout: 30000,
      cacheStrategy: 'network-first',
      retry: {
        maxAttempts: 3,
        backoff: 'exponential',
      },
    });

    this.client.addRequestInterceptor((request) => {
      const headers = new Headers(request.headers);
      headers.set('Authorization', `Bearer ${this.apiKey}`);
      headers.set('Content-Type', 'application/json');
      return new Request(request, { headers });
    });
  }

  async getUser(id: string): Promise<User> {
    return this.client.getJson<User>(`${this.baseUrl}/users/${id}`);
  }

  async createUser(data: CreateUserData): Promise<User> {
    return this.client.postJson<User>(`${this.baseUrl}/users`, data);
  }

  async updateUser(id: string, data: Partial<User>): Promise<User> {
    return this.client.putJson<User>(`${this.baseUrl}/users/${id}`, data);
  }

  async deleteUser(id: string): Promise<void> {
    await this.client.deleteJson(`${this.baseUrl}/users/${id}`);
  }
}

const api = new ApiClient('https://api.example.com', 'my-api-key');
const user = await api.getUser('123');
```

## Migration from v3

Version 4.0 is a **complete rewrite** of `@push.rocks/webrequest`. The v3 API has been removed entirely.

### Key Changes

| v3 | v4 |
|---|---|
| `new WebRequest()` | `webrequest()` function or `new WebrequestClient()` |
| `client.getJson(url, true)` | `webrequest.getJson(url, { cacheStrategy: 'cache-first' })` |
| `client.requestMultiEndpoint([...urls])` | `webrequest(url, { fallbackUrls: [...] })` |
| `request(url, { timeoutMs: 30000 })` | `webrequest(url, { timeout: 30000 })` |

### Migration Examples

```typescript
// v3 — Class-based
import { WebRequest } from '@push.rocks/webrequest';
const client = new WebRequest();
const response = await client.request('https://api.example.com/data', { method: 'GET' });

// v4 — Function-based (fetch-compatible)
import { webrequest } from '@push.rocks/webrequest';
const response = await webrequest('https://api.example.com/data');
const data = await response.json();

// v4 — Client-based (when you need defaults)
import { WebrequestClient } from '@push.rocks/webrequest';
const client = new WebrequestClient({ timeout: 30000 });
const data = await client.getJson('https://api.example.com/data');
```

## License and Legal Information

This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license.md](./license.md) file.

**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

### Trademarks

This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.

Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.

### Company Information

Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany

For any legal inquiries or further information, please contact us via email at hello@task.vc.

By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

---
_Source: https://npm.io/package/@push.rocks/webrequest · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
