CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-make-fetch-happen

Opinionated, caching, retrying fetch client for Node.js with enterprise-grade HTTP features

74

1.68x
Overview
Eval results
Files

caching.mddocs/

HTTP Caching

Advanced HTTP caching system that follows RFC 7234 specifications with cache control, ETags, conditional requests, and intelligent cache validation.

Capabilities

Cache Configuration

Configure HTTP caching behavior through options that control cache storage, retrieval, and validation policies.

/**
 * Cache configuration options
 */
interface CacheOptions {
  cachePath?: string;          // Path to cache directory (required for caching)
  cache?: CacheMode;           // Cache mode controlling behavior
  cacheAdditionalHeaders?: string[];  // Additional headers to store in cache
}

/**
 * Cache modes following fetch specification
 */
type CacheMode = 
  | 'default'        // Standard caching with revalidation
  | 'no-store'       // No caching
  | 'reload'         // Bypass cache, update with response
  | 'no-cache'       // Always revalidate
  | 'force-cache'    // Use cache regardless of staleness
  | 'only-if-cached'; // Only use cached responses, error if none

Usage Examples:

const fetch = require('make-fetch-happen');

// Enable caching with default behavior
const response = await fetch('https://api.example.com/data', {
  cachePath: './my-cache'
});

// Different cache modes
const cachedFetch = fetch.defaults({ cachePath: './cache' });

// Use cache regardless of staleness
const staleResponse = await cachedFetch('https://api.example.com/config', {
  cache: 'force-cache'
});

// Always revalidate before using cache
const freshResponse = await cachedFetch('https://api.example.com/users', {
  cache: 'no-cache'
});

// Only use cache, fail if not cached
try {
  const offlineResponse = await cachedFetch('https://api.example.com/offline-data', {
    cache: 'only-if-cached'
  });
} catch (error) {
  console.log('Not cached:', error.code); // ENOTCACHED
}

Cache Modes

Detailed behavior of each cache mode:

/**
 * Cache mode behaviors:
 * 
 * 'default': Check cache for fresh response, use it. If stale, make conditional
 *           request for revalidation. If network fails, use stale cache.
 * 
 * 'no-store': Never use cache, never store responses in cache.
 * 
 * 'reload': Always make network request, store response in cache.
 * 
 * 'no-cache': Always make conditional request if cached response exists,
 *            otherwise make normal request. Store response in cache.
 * 
 * 'force-cache': Use any cached response regardless of staleness. If no
 *               cached response, make normal request and store in cache.
 * 
 * 'only-if-cached': Use cached response regardless of staleness. If no
 *                  cached response, throw ENOTCACHED error.
 */

Additional Cache Headers

Store custom headers in the cache beyond the standard set:

/**
 * Default headers stored in cache:
 * cache-control, content-encoding, content-language, content-type,
 * date, etag, expires, last-modified, link, location, pragma, vary
 * 
 * Add custom headers with cacheAdditionalHeaders option
 */

Usage Examples:

// Store custom headers in cache
const response = await fetch('https://api.example.com/data', {
  cachePath: './cache',
  cacheAdditionalHeaders: [
    'x-rate-limit-remaining',
    'x-api-version',
    'x-request-id'
  ]
});

// Custom headers will be available on cached responses
const cachedResponse = await fetch('https://api.example.com/data', {
  cachePath: './cache',
  cache: 'force-cache'
});

console.log(cachedResponse.headers.get('x-rate-limit-remaining'));

Cache Response Headers

make-fetch-happen adds metadata headers to cached responses:

/**
 * Special headers added to cached responses:
 */
interface CacheResponseHeaders {
  'x-local-cache': string;        // Path to cache directory
  'x-local-cache-key': string;    // Unique cache key for this response
  'x-local-cache-mode': 'stream'; // Always 'stream'
  'x-local-cache-hash': string;   // Integrity hash of cached content
  'x-local-cache-status': CacheStatus; // How response was generated
  'x-local-cache-time': string;   // ISO timestamp of cache insertion
}

type CacheStatus = 
  | 'miss'        // Not in cache, fetched from network
  | 'hit'         // Fresh cache hit
  | 'stale'       // Stale cache hit (returned without revalidation)
  | 'revalidated' // Stale cache revalidated (304 response)
  | 'updated'     // Stale cache updated with new response
  | 'skip';       // Cache bypassed

Usage Examples:

const response = await fetch('https://api.example.com/data', {
  cachePath: './cache'
});

// Check cache status
const cacheStatus = response.headers.get('x-local-cache-status');
console.log('Cache status:', cacheStatus); // 'miss', 'hit', etc.

// Get cache timing information
const cacheTime = response.headers.get('x-local-cache-time');
if (cacheTime) {
  console.log('Cached at:', new Date(cacheTime));
}

Manual Cache Access

Access cached entries directly using cacache:

/**
 * Manual cache access (requires cacache dependency)
 */
const cacache = require('cacache');

// Get cached entry using response headers
const getCachedEntry = async (response) => {
  const cachePath = response.headers.get('x-local-cache');
  const cacheKey = response.headers.get('x-local-cache-key');
  
  if (cachePath && cacheKey) {
    return await cacache.get(cachePath, cacheKey);
  }
};

// Get content by integrity hash
const getCachedContent = async (response) => {
  const cachePath = response.headers.get('x-local-cache');
  const cacheHash = response.headers.get('x-local-cache-hash');
  
  if (cachePath && cacheHash) {
    return await cacache.get.byDigest(cachePath, cacheHash);
  }
};

Cache Invalidation

Caches are automatically invalidated for successful non-GET/HEAD requests:

/**
 * Automatic cache invalidation occurs when:
 * - Request method is not GET or HEAD
 * - Response status is 200-399
 * - Request has a cachePath configured
 * 
 * This ensures cached data remains consistent after mutations
 */

Usage Examples:

const apiFetch = fetch.defaults({ cachePath: './cache' });

// GET request - uses and populates cache
const userData = await apiFetch('https://api.example.com/users/123');

// POST request - invalidates cache for this URL
await apiFetch('https://api.example.com/users/123', {
  method: 'POST',
  body: JSON.stringify({ name: 'Updated Name' }),
  headers: { 'Content-Type': 'application/json' }
});

// Next GET will fetch fresh data
const updatedData = await apiFetch('https://api.example.com/users/123');

Cache Storage Requirements

Caching behavior depends on proper cache directory setup:

/**
 * Cache requirements:
 * - cachePath must be specified to enable caching
 * - Directory must be writable by the process
 * - Only GET and HEAD requests are cached
 * - Responses must be consumed (json(), text(), buffer(), etc.) to be cached
 */

Usage Examples:

// Ensure response is consumed for caching
const response = await fetch('https://api.example.com/data', {
  cachePath: './cache'
});

// Must consume response body for caching to work
const data = await response.json(); // or .text(), .buffer(), etc.

// Stream responses are also cached
const streamResponse = await fetch('https://api.example.com/large-file', {
  cachePath: './cache'
});

// Pipe to destination (response will be cached)
streamResponse.body.pipe(fs.createWriteStream('./downloaded-file'));

Install with Tessl CLI

npx tessl i tessl/npm-make-fetch-happen

docs

caching.md

configuration.md

core-fetch.md

index.md

network.md

retry.md

security.md

tile.json