2024-04-14 18:41:19 +02:00
# @push.rocks/webrequest
feat: Implement comprehensive web request handling with caching, retry, and interceptors
- Added cache strategies: NetworkFirst, CacheFirst, StaleWhileRevalidate, NetworkOnly, and CacheOnly.
- Introduced InterceptorManager for managing request, response, and error interceptors.
- Developed RetryManager for handling request retries with customizable backoff strategies.
- Implemented RequestDeduplicator to prevent simultaneous identical requests.
- Created timeout utilities for handling request timeouts.
- Enhanced WebrequestClient to support global interceptors, caching, and retry logic.
- Added convenience methods for common HTTP methods (GET, POST, PUT, DELETE) with JSON handling.
- Established a fetch-compatible webrequest function for seamless integration.
- Defined core type structures for caching, retry options, interceptors, and web request configurations.
2025-10-20 09:59:24 +00:00
2026-03-02 13:49:27 +00:00
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.
2019-06-04 15:40:30 +02:00
2025-10-20 13:41:03 +00:00
## Features
2024-04-14 18:41:19 +02:00
2026-03-02 13:49:27 +00:00
- 🌐 **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
2025-10-20 13:41:03 +00:00
## Installation
2024-04-14 18:41:19 +02:00
```bash
2025-10-20 13:41:03 +00:00
pnpm install @push .rocks/webrequest
# or
npm install @push .rocks/webrequest
2024-04-14 18:41:19 +02:00
```
2025-10-20 13:41:03 +00:00
This package requires a modern JavaScript environment with ESM and TypeScript support.
2019-06-04 15:40:30 +02:00
2025-10-20 13:41:03 +00:00
## Quick Start
2019-06-04 15:40:30 +02:00
2025-10-20 13:41:03 +00:00
### Basic Fetch-Compatible Usage
2024-04-14 18:41:19 +02:00
2025-10-20 13:41:03 +00:00
```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,
2026-03-02 13:49:27 +00:00
retry: true,
2025-10-20 13:41:03 +00:00
});
```
2024-04-14 18:41:19 +02:00
2025-10-20 13:41:03 +00:00
### JSON Convenience Methods
2024-04-14 18:41:19 +02:00
```typescript
2025-10-20 13:41:03 +00:00
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',
2026-03-02 13:49:27 +00:00
email: 'john@example .com',
2025-10-20 13:41:03 +00:00
});
2026-03-02 13:49:27 +00:00
// PUT and DELETE
2025-10-20 13:41:03 +00:00
await webrequest.putJson(url, data);
await webrequest.deleteJson(url);
2024-04-14 18:41:19 +02:00
```
2025-10-20 13:41:03 +00:00
## Cache Strategies
### Network-First (Default)
Always fetch from network, fall back to cache on failure. Respects HTTP caching headers.
2024-04-14 18:41:19 +02:00
```typescript
2025-10-20 13:41:03 +00:00
const data = await webrequest.getJson('https://api.example.com/data', {
2026-03-02 13:49:27 +00:00
cacheStrategy: 'network-first',
2024-04-14 18:41:19 +02:00
});
```
2025-10-20 13:41:03 +00:00
### Cache-First
2024-04-14 18:41:19 +02:00
2025-10-20 13:41:03 +00:00
Check cache first, only fetch from network if not cached or stale.
2024-04-14 18:41:19 +02:00
```typescript
2025-10-20 13:41:03 +00:00
const data = await webrequest.getJson('https://api.example.com/data', {
cacheStrategy: 'cache-first',
2026-03-02 13:49:27 +00:00
cacheMaxAge: 60000, // 60 seconds
2025-10-20 13:41:03 +00:00
});
```
### Stale-While-Revalidate
Return cached data immediately, update in background.
```typescript
const data = await webrequest.getJson('https://api.example.com/data', {
2026-03-02 13:49:27 +00:00
cacheStrategy: 'stale-while-revalidate',
2025-10-20 13:41:03 +00:00
});
```
### Network-Only and Cache-Only
```typescript
// Always fetch from network, never cache
2026-03-02 13:49:27 +00:00
const fresh = await webrequest.getJson(url, {
cacheStrategy: 'network-only',
2025-10-20 13:41:03 +00:00
});
// Only use cache, never fetch from network
2026-03-02 13:49:27 +00:00
const cached = await webrequest.getJson(url, {
cacheStrategy: 'cache-only',
2025-10-20 13:41:03 +00:00
});
```
### HTTP Header-Based Caching
The library automatically respects HTTP caching headers:
```typescript
// Server returns: Cache-Control: max-age=3600, ETag: "abc123"
const response = await webrequest('https://api.example.com/data', {
2026-03-02 13:49:27 +00:00
cacheStrategy: 'network-first',
2025-10-20 13:41:03 +00:00
});
// Subsequent requests automatically send:
// If-None-Match: "abc123"
2026-03-02 13:49:27 +00:00
// Server returns 304 Not Modified — cache is used
2025-10-20 13:41:03 +00:00
```
### 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')}` ;
2026-03-02 13:49:27 +00:00
},
2025-10-20 13:41:03 +00:00
});
```
## Retry Strategies
### Basic Retry
```typescript
const response = await webrequest('https://api.example.com/data', {
2026-03-02 13:49:27 +00:00
retry: true, // Uses defaults: 3 attempts, exponential backoff
2025-10-20 13:41:03 +00:00
});
```
### Advanced Retry Configuration
2024-04-14 18:41:19 +02:00
2025-10-20 13:41:03 +00:00
```typescript
const response = await webrequest('https://api.example.com/data', {
retry: {
maxAttempts: 5,
backoff: 'exponential', // or 'linear', 'constant'
2026-03-02 13:49:27 +00:00
initialDelay: 1000, // 1 second
maxDelay: 30000, // 30 seconds
retryOn: [408, 429, 500, 502, 503, 504],
2025-10-20 13:41:03 +00:00
onRetry: (attempt, error, nextDelay) => {
console.log(`Retry attempt ${attempt}, waiting ${nextDelay}ms` );
2026-03-02 13:49:27 +00:00
},
},
2025-10-20 13:41:03 +00:00
});
2024-04-14 18:41:19 +02:00
```
2025-10-20 13:41:03 +00:00
### Multi-Endpoint Fallback
2026-03-02 13:49:27 +00:00
When the primary endpoint fails, automatically try fallback URLs:
2025-10-20 13:41:03 +00:00
```typescript
const response = await webrequest('https://api1.example.com/data', {
fallbackUrls: [
'https://api2.example.com/data',
2026-03-02 13:49:27 +00:00
'https://api3.example.com/data',
2025-10-20 13:41:03 +00:00
],
retry: {
maxAttempts: 3,
2026-03-02 13:49:27 +00:00
backoff: 'exponential',
},
2025-10-20 13:41:03 +00:00
});
```
2024-04-14 18:41:19 +02:00
2026-03-02 13:49:27 +00:00
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.
2025-10-20 13:41:03 +00:00
## Request/Response Interceptors
### Global Interceptors
2024-04-14 18:41:19 +02:00
```typescript
2025-10-20 13:41:03 +00:00
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 });
});
2024-04-14 18:41:19 +02:00
2025-10-20 13:41:03 +00:00
// 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);
2026-03-02 13:49:27 +00:00
return error;
2025-10-20 13:41:03 +00:00
});
2026-03-02 13:49:27 +00:00
// Clear all interceptors when needed
webrequest.clearInterceptors();
2025-10-20 13:41:03 +00:00
```
### 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;
}],
2026-03-02 13:49:27 +00:00
},
2025-10-20 13:41:03 +00:00
});
```
## Request Deduplication
2024-04-14 18:41:19 +02:00
2025-10-20 13:41:03 +00:00
Automatically prevent duplicate simultaneous requests:
2024-04-14 18:41:19 +02:00
2025-10-20 13:41:03 +00:00
```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 }),
]);
2026-03-02 13:49:27 +00:00
// All three get the same response (cloned)
2024-04-14 18:41:19 +02:00
```
2026-03-02 13:49:27 +00:00
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
2024-04-14 18:41:19 +02:00
2026-03-02 13:49:27 +00:00
For more control, use `WebrequestClient` to set default options that apply to all requests made through that client:
2024-04-14 18:41:19 +02:00
```typescript
2025-10-20 13:41:03 +00:00
import { WebrequestClient } from '@push .rocks/webrequest';
const apiClient = new WebrequestClient({
logging: true,
timeout: 30000,
cacheStrategy: 'network-first',
retry: {
maxAttempts: 3,
2026-03-02 13:49:27 +00:00
backoff: 'exponential',
},
2025-10-20 13:41:03 +00:00
});
2024-04-14 18:41:19 +02:00
2025-10-20 13:41:03 +00:00
// Add global interceptors to this client
apiClient.addRequestInterceptor((request) => {
const headers = new Headers(request.headers);
2026-03-02 13:49:27 +00:00
headers.set('X-API-Key', 'my-key');
2025-10-20 13:41:03 +00:00
return new Request(request, { headers });
});
// All requests through this client use the configured defaults
const data = await apiClient.getJson('https://api.example.com/data');
2026-03-02 13:49:27 +00:00
// Standard fetch-compatible API
2025-10-20 13:41:03 +00:00
const response = await apiClient.request('https://api.example.com/data');
2026-03-02 13:49:27 +00:00
// Create a client from the webrequest function
const client = webrequest.createClient({ timeout: 5000 });
2024-04-14 18:41:19 +02:00
```
2025-10-20 13:41:03 +00:00
## Advanced Features
2024-04-14 18:41:19 +02:00
2025-10-20 13:41:03 +00:00
### Timeout
2024-04-14 18:41:19 +02:00
```typescript
2025-10-20 13:41:03 +00:00
const response = await webrequest('https://api.example.com/data', {
2026-03-02 13:49:27 +00:00
timeout: 5000, // 5 seconds — throws Error on timeout
2025-10-20 13:41:03 +00:00
});
```
### Cache Management
```typescript
// Clear all cached responses
await webrequest.clearCache();
2024-04-14 18:41:19 +02:00
```
2025-10-20 13:41:03 +00:00
## API Reference
2024-04-14 18:41:19 +02:00
2025-10-20 13:41:03 +00:00
### Main Function
2024-04-14 18:41:19 +02:00
```typescript
2025-10-20 13:41:03 +00:00
webrequest(input: string | Request | URL, options?: IWebrequestOptions): Promise<Response>
```
### 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>
```
### Global Methods
```typescript
webrequest.addRequestInterceptor(interceptor: TRequestInterceptor): void
webrequest.addResponseInterceptor(interceptor: TResponseInterceptor): void
webrequest.addErrorInterceptor(interceptor: TErrorInterceptor): void
2026-03-02 13:49:27 +00:00
webrequest.clearInterceptors(): void
webrequest.clearCache(): Promise<void>
webrequest.createClient(options?: Partial<IWebrequestOptions>): WebrequestClient
webrequest.getDefaultClient(): WebrequestClient
2025-10-20 13:41:03 +00:00
```
### Options Interface
```typescript
interface IWebrequestOptions extends Omit<RequestInit, 'cache'> {
// Standard fetch options
method?: string;
headers?: HeadersInit;
body?: BodyInit;
2026-03-02 13:49:27 +00:00
// Caching
2025-10-20 13:41:03 +00:00
cache?: 'default' | 'no-store' | 'reload' | 'no-cache' | 'force-cache' | 'only-if-cached';
cacheStrategy?: 'network-first' | 'cache-first' | 'stale-while-revalidate' | 'network-only' | 'cache-only';
2026-03-02 13:49:27 +00:00
cacheMaxAge?: number;
cacheKey?: string | ((request: Request) => string);
revalidate?: boolean;
2025-10-20 13:41:03 +00:00
2026-03-02 13:49:27 +00:00
// Retry & Fault Tolerance
2025-10-20 13:41:03 +00:00
retry?: boolean | IRetryOptions;
fallbackUrls?: string[];
2026-03-02 13:49:27 +00:00
timeout?: number;
2025-10-20 13:41:03 +00:00
2026-03-02 13:49:27 +00:00
// Interceptors
2025-10-20 13:41:03 +00:00
interceptors?: {
request?: TRequestInterceptor[];
response?: TResponseInterceptor[];
};
2026-03-02 13:49:27 +00:00
// Deduplication
2025-10-20 13:41:03 +00:00
deduplicate?: boolean;
2026-03-02 13:49:27 +00:00
// Logging
2025-10-20 13:41:03 +00:00
logging?: boolean;
2024-04-14 18:41:19 +02:00
}
2025-10-20 13:41:03 +00:00
```
2026-03-02 13:49:27 +00:00
### Retry Options
2024-04-14 18:41:19 +02:00
2025-10-20 13:41:03 +00:00
```typescript
2026-03-02 13:49:27 +00:00
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;
}
2025-10-20 13:41:03 +00:00
```
## 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;
}
2024-04-14 18:41:19 +02:00
2025-10-20 13:41:03 +00:00
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',
2026-03-02 13:49:27 +00:00
retryOn: [500, 502, 503, 504],
2025-10-20 13:41:03 +00:00
},
fallbackUrls: [
2026-03-02 13:49:27 +00:00
`https://api-backup.example.com/users/${userId}` ,
2025-10-20 13:41:03 +00:00
],
2026-03-02 13:49:27 +00:00
timeout: 10000,
2025-10-20 13:41:03 +00:00
deduplicate: true,
interceptors: {
request: [(req) => {
console.log(`Fetching user ${userId}` );
return req;
2026-03-02 13:49:27 +00:00
}],
},
2025-10-20 13:41:03 +00:00
}
);
return user;
}
```
### Building a Typed API Client
```typescript
import { WebrequestClient } from '@push .rocks/webrequest';
2026-03-02 13:49:27 +00:00
interface User {
id: string;
name: string;
email: string;
}
interface CreateUserData {
name: string;
email: string;
}
2025-10-20 13:41:03 +00:00
class ApiClient {
private client: WebrequestClient;
constructor(private baseUrl: string, private apiKey: string) {
this.client = new WebrequestClient({
timeout: 30000,
cacheStrategy: 'network-first',
retry: {
maxAttempts: 3,
2026-03-02 13:49:27 +00:00
backoff: 'exponential',
},
2025-10-20 13:41:03 +00:00
});
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);
}
2026-03-02 13:49:27 +00:00
async updateUser(id: string, data: Partial<User>): Promise<User> {
2025-10-20 13:41:03 +00:00
return this.client.putJson<User>(`${this.baseUrl}/users/${id}` , data);
}
async deleteUser(id: string): Promise<void> {
await this.client.deleteJson(`${this.baseUrl}/users/${id}` );
}
}
2026-03-02 13:49:27 +00:00
const api = new ApiClient('https://api.example.com', 'my-api-key');
2025-10-20 13:41:03 +00:00
const user = await api.getUser('123');
```
2024-04-14 18:41:19 +02:00
2026-03-02 13:49:27 +00:00
## 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');
```
2024-04-14 18:41:19 +02:00
## License and Legal Information
2026-03-02 13:49:27 +00:00
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE ](./LICENSE ) file.
2024-04-14 18:41:19 +02:00
**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
2026-03-02 13:49:27 +00:00
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.
2020-06-25 22:45:19 +00:00
2024-04-14 18:41:19 +02:00
### Company Information
2020-06-25 22:45:19 +00:00
2025-10-20 13:41:03 +00:00
Task Venture Capital GmbH
2026-03-02 13:49:27 +00:00
Registered at District Court Bremen HRB 35230 HB, Germany
2020-06-25 22:45:19 +00:00
2026-03-02 13:49:27 +00:00
For any legal inquiries or further information, please contact us via email at hello@task .vc.
2019-06-04 15:40:30 +02:00
2024-04-14 18:41:19 +02:00
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.