Skip to content

Latest commit

 

History

History
943 lines (687 loc) · 25.3 KB

File metadata and controls

943 lines (687 loc) · 25.3 KB

Axios Cache Interceptor - AI Assistant Guide

This document provides comprehensive context for AI assistants working with the axios-cache-interceptor codebase.

Project Overview

axios-cache-interceptor is a high-performance, feature-rich caching layer for Axios HTTP client. It intercepts requests and responses to provide intelligent caching with minimal configuration.

Core Purpose

  • Prevent redundant network requests through smart caching
  • Handle concurrent requests efficiently (request deduplication)
  • Support HTTP caching standards (Cache-Control, ETag, Vary, etc.)
  • Provide flexible storage backends (memory, localStorage, custom)
  • Work seamlessly in both browser and Node.js environments

Key Features

  • Automatic request deduplication: Concurrent identical requests share a single network call
  • HTTP standard compliance: Respects Cache-Control, ETag, If-None-Match, If-Modified-Since, Vary headers
  • Stale-while-revalidate: Serve stale cache while fetching fresh data
  • Flexible storage: Built-in memory and web storage, extensible for Redis, IndexedDB, etc.
  • Per-request configuration: Override global cache settings per request
  • TypeScript-first: Fully typed with excellent IDE support
  • Development builds: Separate debug builds with comprehensive logging

Project Structure

src/
├── cache/          # Core cache types and setup
│   ├── axios.ts    # Extended Axios types (AxiosCacheInstance, CacheAxiosResponse)
│   ├── cache.ts    # Cache configuration interfaces (CacheProperties, CacheInstance)
│   └── create.ts   # setupCache() - main entry point
├── interceptors/   # Request/response interception logic
│   ├── request.ts  # Request interceptor (cache lookup, concurrent handling)
│   ├── response.ts # Response interceptor (cache storage, error handling)
│   ├── util.ts     # Shared interceptor utilities
│   └── build.ts    # Interceptor types
├── storage/        # Storage adapters
│   ├── types.ts    # StorageValue types (cached/stale/loading/empty)
│   ├── build.ts    # buildStorage() - storage builder
│   ├── memory.ts   # In-memory storage with eviction
│   └── web-api.ts  # localStorage/sessionStorage adapter
├── header/         # HTTP header interpretation
│   ├── interpreter.ts # Parse Cache-Control, Expires
│   ├── extract.ts     # Extract headers for Vary support
│   └── headers.ts     # Header name constants
├── util/           # Utilities
│   ├── key-generator.ts    # Request ID generation (defaultKeyGenerator)
│   ├── cache-predicate.ts  # Response cacheability tests
│   ├── update-cache.ts     # Cache invalidation/updates
│   └── types.ts            # Shared types
└── index.ts        # Public API exports

test/               # Test suite (node:test)
docs/               # VitePress documentation

Core Concepts

1. Request Lifecycle

Request Flow:

User Request
  → Request Interceptor
    → Generate Request ID (key)
    → Check cache (storage.get)
    → If cached: Return cached response
    → If loading: Wait for concurrent request
    → If empty/stale: Make network request
  → Network (Axios adapter)
  → Response Interceptor
    → Interpret headers (TTL, ETag, etc.)
    → Test cache predicate
    → Store in cache (storage.set)
    → Resolve waiting concurrent requests
  → User receives response

2. Storage States

Every cache entry has one of these states:

  • empty: No cached data exists
  • cached: Valid cached data within TTL
  • stale: Expired data that can be revalidated (ETag/Last-Modified)
  • must-revalidate: Cache-Control: must-revalidate (similar to stale but stricter)
  • loading: Request in progress
    • previous: 'empty': First request
    • previous: 'stale': Revalidating stale data
    • previous: 'must-revalidate': Revalidating with must-revalidate

3. Request ID Generation

Each request gets a unique ID (cache key) generated by KeyGenerator:

  • Uses config.id if provided (manual override)
  • Otherwise generates hash from: method, baseURL, url, params, data
  • With Vary support: Also includes subset of request headers
  • Uses object-code library for 32-bit hash (collision risk at ~77k keys)

4. Concurrent Request Handling

Critical for performance:

  • First request sets cache state to loading
  • Creates Deferred promise in waiting Map
  • Subsequent identical requests wait on this deferred
  • When response arrives, resolves/rejects all waiting requests
  • Prevents multiple network calls for same resource

5. Vary Header Support

HTTP Vary header support for content negotiation:

  • Server sends Vary: Authorization, Accept-Language
  • Library extracts specified headers from request
  • Includes them in cache key (via metadata)
  • Different header values = different cache entries
  • Vary: * immediately marks cache as stale (must revalidate every time)

6. Stale Revalidation

Support for ETag and If-Modified-Since:

  • ETag: Stores response ETag, sends If-None-Match on revalidation
  • Last-Modified: Stores Last-Modified or cache timestamp, sends If-Modified-Since
  • 304 Not Modified: Keeps existing cache data, updates timestamps
  • Enabled by default, controlled by cache.etag and cache.modifiedSince

7. Hydrate Pattern

Optimistic UI updates:

  • When making network request but stale cache exists
  • Calls cache.hydrate(staleData) immediately
  • Developer can update UI with old data
  • Network response updates UI with fresh data
  • Prevents UI flicker on slow networks

Key Files Deep Dive

src/cache/create.ts - setupCache()

Entry point for library. Takes Axios instance and returns enhanced instance.

Key responsibilities:

  • Validates single initialization (prevents double setup)
  • Initializes storage (defaults to memory)
  • Sets up interceptors
  • Merges global and per-request config
  • Sets default cache properties

Default configuration:

  • ttl: 5 minutes
  • methods: ['get', 'head']
  • cachePredicate: RFC 7231 cacheable status codes
  • interpretHeader: true
  • etag: true
  • modifiedSince: false (unless etag disabled)
  • vary: true
  • staleIfError: true

src/interceptors/request.ts - Request Interceptor

Most complex logic in the library.

Key operations:

  1. Generate request ID
  2. Check if caching disabled (cache: false or enabled: false)
  3. Check URL filters (ignoreUrls, allowUrls)
  4. Apply cacheTakeover headers (prevent browser cache)
  5. Validate method against cache.methods
  6. Get cached value
  7. Handle Vary mismatches (regenerate key if needed)
  8. Handle concurrent requests (wait on deferred)
  9. Set up loading state for new requests
  10. Add conditional headers for stale revalidation
  11. Call hydrate callback if available

Critical sections:

  • Concurrent request detection: Checks axios.waiting Map
  • Vary mismatch handling: Switches to new cache key mid-flight
  • Custom adapter injection: Returns cached data without network

src/interceptors/response.ts - Response Interceptor

Handles successful and failed responses.

Key operations:

  1. Skip if already cached (from request interceptor)
  2. Update other caches (cache.update)
  3. Test cache predicate (should this be cached?)
  4. Interpret headers for TTL (Cache-Control, Expires)
  5. Store cache metadata (ETag, Last-Modified, Vary headers)
  6. Handle Vary: * (immediately mark as stale)
  7. Resolve waiting concurrent requests
  8. Error handling: Implement stale-if-error

Error interceptor logic:

  • Check if cache should be returned despite error
  • Respect cache.staleIfError configuration
  • Can use stale cache for network errors
  • Re-marks cache as stale for future requests

src/storage/types.ts - Storage Contract

Critical type definitions. All storage implementations must follow this interface.

Key types:

  • StorageValue: Union of all possible states
  • CachedResponse: Stored response data structure
  • CachedResponseMeta: Metadata (vary headers, revalidation info)
  • AxiosStorage: Interface for storage implementations

Storage methods:

  • get(key): Returns StorageValue (never undefined, returns {state: 'empty'})
  • set(key, value): Stores NotEmptyStorageValue
  • remove(key): Deletes cache entry
  • clear(): Optional, clears all (not used by interceptor)

src/storage/memory.ts - Memory Storage

Default storage implementation.

Features:

  • JavaScript Map for O(1) lookups
  • Optional data cloning (clone option) to prevent mutation
  • Automatic cleanup interval
  • FIFO eviction with maxEntries
  • maxStaleAge to prevent memory leaks from stale entries
  • Smart eviction ordering (prefers expired over stale over cached)

Gotchas:

  • Without cloning, mutations affect cached data
  • Long-running processes need cleanup interval
  • Default 1024 entry limit may need adjustment

src/storage/web-api.ts - Web Storage

localStorage/sessionStorage adapter.

Features:

  • JSON serialization
  • Key prefixing to avoid collisions
  • Quota exceeded handling (auto-eviction)
  • maxStaleAge for cleanup

Eviction strategy on quota exceeded:

  1. Remove all expired entries
  2. Remove oldest entry
  3. Retry save
  4. Repeat until success or storage empty

src/util/key-generator.ts - Request ID

Generates unique IDs for cache keys.

Default generator considers:

  • method (lowercase normalized)
  • baseURL (trailing slashes removed)
  • url (trailing slashes removed)
  • params (query parameters)
  • data (request body)
  • meta.vary (Vary header values)

Uses object-code library for hashing:

  • 32-bit signed integer hash
  • Fast but collision risk at ~77k unique requests
  • Custom generator recommended for large-scale persistent cache

src/header/interpreter.ts - Header Interpretation

Interprets HTTP caching headers.

Implements:

  • Cache-Control: max-age, s-maxage, no-cache, no-store, must-revalidate, private, public
  • Expires: Absolute expiration date
  • Age: Response age from proxy
  • stale-if-error: Grace period for errors
  • stale-while-revalidate: Background revalidation

Returns:

  • 'dont cache': No caching (no-cache, no-store)
  • 'not enough headers': Use TTL from config
  • number: TTL in milliseconds
  • { cache: number, stale: number }: TTL and stale TTL

Configuration Guide

Global Configuration (setupCache)

All options in CacheProperties can be set globally:

const axios = setupCache(instance, {
  // Cache defaults for all requests
  ttl: 1000 * 60 * 15,
  methods: ['get', 'head', 'post'],
  storage: buildMemoryStorage(),
  debug: console.log,

  // Less common options
  generateKey: customKeyGenerator,
  headerInterpreter: customInterpreter,
  cacheTakeover: true,
  interpretHeader: true,
  vary: true,
  staleIfError: true,
  etag: true,
  modifiedSince: false
});

Per-Request Configuration

Override global settings per request:

await axios.get('/api/data', {
  // Custom request ID
  id: 'my-custom-id',

  cache: {
    // Override any global option
    ttl: 1000 * 60 * 60,
    enabled: false,
    override: true,

    // Per-request specific
    update: { [otherRequestId]: 'delete' },
    hydrate: async (cache) => updateUI(cache.data),

    cachePredicate: {
      statusCheck: [200, 201],
      responseMatch: (res) => res.data.cacheable,
      ignoreUrls: [/\/admin/],
      allowUrls: ['/api/public']
    }
  }
});

Testing

Test Framework

  • Uses Node.js built-in node:test (no external framework)
  • Assertions via node:assert
  • Located in test/**/*.test.ts

Test Utilities

test/mocks/axios.js: Mock Axios instance

  • Returns mockAxios(options) - pre-configured instance
  • Includes mock adapter that returns successful responses
  • Pre-wrapped with setupCache()

test/utils.ts: Test helpers

  • mockDateNow(): Mock Date.now() for time-based tests
  • Cleanup utilities

Running Tests

npm test              # Run all tests with coverage
npm run test:only     # Run tests marked with .only
npm run test:types    # TypeScript type checking

Test Patterns

Concurrent request testing:

const [resp1, resp2] = await Promise.all([
  axios.get('http://test.com'),
  axios.get('http://test.com')
]);
assert.equal(resp1.cached, false);
assert.ok(resp2.cached);

Storage state testing:

const response = await axios.get('url');
const cache = await axios.storage.get(response.id);
assert.equal(cache.state, 'cached');

Cache invalidation testing:

const cacheKey = axios.generateKey({
  url: '/api/data',
  method: 'get'
});
await axios.storage.remove(cacheKey);
const cache = await axios.storage.get(cacheKey);
assert.equal(cache.state, 'empty');

Development Guidelines

Adding New Features

  1. Understand the interceptor flow - Most features touch request/response interceptors
  2. Consider storage state transitions - How does feature affect state machine?
  3. Think about concurrent requests - Will feature work with deduplication?
  4. HTTP compliance - Check RFC specs for caching behavior
  5. Add tests - Cover normal case, edge cases, concurrent scenarios
  6. Update TypeScript types - Maintain type safety
  7. Document in VitePress - Add to docs/src/

Modifying Interceptors

Request interceptor (src/interceptors/request.ts):

  • Runs before network request
  • Critical: Handle cache state transitions carefully
  • Must update waiting Map correctly
  • Consider Vary header implications
  • Don't break concurrent request handling

Response interceptor (src/interceptors/response.ts):

  • Runs after network response
  • Always resolve/reject deferred promises
  • Clean up waiting Map
  • Handle both success and error paths
  • Consider cache predicate carefully

Storage Implementation

To create custom storage:

import { buildStorage } from 'axios-cache-interceptor';

const myStorage = buildStorage({
  async find(key) {
    // Return StorageValue or undefined
  },

  async set(key, value) {
    // Store value
    // Handle TTL/expiration if backend supports it
  },

  async remove(key) {
    // Delete key
  },

  async clear() {
    // Optional: clear all keys
  }
});

Important considerations:

  • find() should handle expired entries (return undefined or correct state)
  • set() receives value.state which can be 'loading', 'cached', 'stale'
  • For auto-expiring backends (Redis), calculate expiration time carefully
  • Stale entries should live longer than fresh TTL (they're revalidatable)

Header Interpretation

Custom header interpreter:

import {
  setupCache,
  type HeaderInterpreter
} from 'axios-cache-interceptor';

const myInterpreter: HeaderInterpreter = (headers, location) => {
  // Return: 'dont cache' | 'not enough headers' | number | {cache: number, stale: number}

  if (headers['x-custom-cache']) {
    return Number(headers['x-custom-cache']) * 1000;
  }

  return 'not enough headers'; // Fall back to config.ttl
};

Key Generation

Custom key generator for better collision resistance:

import { buildKeyGenerator } from 'axios-cache-interceptor';
import { createHash } from 'crypto';

const customGenerator = buildKeyGenerator((req, meta) => {
  // Build unique string
  const str = `${req.method}:${req.url}:${JSON.stringify(req.params)}`;

  // Use stronger hash (for persistent storage)
  return createHash('sha256').update(str).digest('hex');
});

setupCache(axios, { generateKey: customGenerator });

Common Patterns

Opt-in Caching

// Disable cache by default
const axios = setupCache(instance, {
  enabled: false
});

// Enable for specific endpoints
axios.get('/expensive-api', {
  cache: { enabled: true, ttl: 1000 * 60 * 10 }
});

Cache Invalidation

// Delete specific cache entry
const response = await axios.get('/api/users');
await axios.storage.remove(response.id);

// Delete by custom ID
await axios.storage.remove('my-custom-id');

// Clear all cache
await axios.storage.clear?.();

// Update multiple caches after mutation
axios.post('/api/users', userData, {
  cache: {
    update: {
      'user-list': 'delete',
      'user-count': 'delete'
    }
  }
});

Debugging Issues

// Use development build
import { setupCache } from 'axios-cache-interceptor/dev';

const axios = setupCache(instance, {
  debug: (obj) => {
    console.log(`[${obj.id}] ${obj.msg}`, obj.data);
  }
});

Debug output shows:

  • Cache hits/misses
  • Concurrent request handling
  • Header interpretation
  • Vary mismatches
  • Error handling decisions

Important Edge Cases

1. Vary: * Responses

Vary: * means response varies by everything (uncacheable in shared caches).

  • Library marks these as immediately stale
  • Forces revalidation every time
  • Storage state: stale with immediate expiration

2. Concurrent Vary Mismatches

Two requests with different headers wait on same initial request:

  • Request A (Authorization: Bearer A) starts
  • Request B (Authorization: Bearer B) waits
  • When A completes, B checks vary headers
  • B detects mismatch, makes own request (doesn't use A's cache)

3. Storage Eviction During Loading

Cache entry evicted while request in-flight:

  • Request sets state to loading
  • Storage runs eviction (LRU/quota)
  • Response arrives, storage state is empty
  • Interceptor handles gracefully (no-op waiting Map cleanup)

4. Request Cancellation

Axios request cancelled mid-flight:

  • Request interceptor started, waiting Map has deferred
  • User cancels request
  • Response interceptor sees ERR_CANCELED
  • Must not clear cache if already cached
  • Must reject deferred to unblock concurrent requests

5. 304 Not Modified

Server returns 304:

  • Request sent with If-None-Match or If-Modified-Since
  • Server validates, returns 304 (no body)
  • Keep existing cached data
  • Update timestamps (extends TTL)
  • Special handling in response interceptor

Build System

Tools

  • tsdown: TypeScript bundler (similar to tsup)
  • TypeScript 5.9+: Strict mode enabled
  • Biome: Linting and formatting
  • c8: Code coverage
  • VitePress: Documentation

Build Configuration

Outputs:

  • dist/index.cjs - CommonJS
  • dist/index.mjs - ES Module
  • dist/index.bundle.js - UMD for browsers (production)
  • dev/index.cjs / dev/index.mjs - Debug builds with __ACI_DEV__ = true

Scripts

npm run build          # Production build
npm run lint           # Check code quality
npm run lint-fix       # Auto-fix issues
npm run format         # Format code
npm test               # Run tests with coverage
npm run test:types     # Type check
npm run docs:dev       # Serve docs locally
npm run docs:build     # Build static docs

Development vs Production

Production build:

  • __ACI_DEV__ constant is false
  • Debug code removed by tree-shaking
  • Minified for size
  • All debug logs stripped

Development build:

  • __ACI_DEV__ constant is true
  • Includes axios.debug() calls
  • Comprehensive logging
  • Slightly larger bundle

Usage:

// Production
import { setupCache } from 'axios-cache-interceptor';

// Development
import { setupCache } from 'axios-cache-interceptor/dev';

Dependencies

Runtime Dependencies

  • axios: >=1.0.0 (peer dependency)
  • cache-parser: Parse Cache-Control headers
  • fast-defer: Efficient deferred promises
  • http-vary: Vary header parsing and comparison
  • object-code: Fast 32-bit hashing
  • try: Safe function execution

Why These Libraries?

  • cache-parser: Robust Cache-Control parsing (many edge cases)
  • fast-defer: Better than manual Promise constructor
  • http-vary: RFC-compliant Vary header handling
  • object-code: Fast deterministic hashing for object keys

Anti-Patterns to Avoid

1. Don't Mutate Cached Data

// BAD - mutates cache
const res = await axios.get('/api/user');
res.data.name = 'Modified';

// GOOD - use cloning storage
setupCache(axios, {
  storage: buildMemoryStorage(true) // cloneData = true
});

2. Don't Ignore Cache Predicate

// BAD - cache predicate failed but developer expects caching
setupCache(axios, {
  cachePredicate: { statusCheck: [200] }
});

// Server returns 201 - won't be cached!

// GOOD - include all success codes you expect
cachePredicate: {
  statusCheck: (status) => status >= 200 && status < 300;
}

3. Don't Use Weak Hashes for Large-Scale Persistent Storage

// BAD - default 32-bit hash, collision risk
setupCache(axios, { storage: redisStorage });

// GOOD - use cryptographic hash
setupCache(axios, {
  storage: redisStorage,
  generateKey: buildKeyGenerator((req) => {
    return createHash('sha256')
      .update(JSON.stringify([req.url, req.method, req.params]))
      .digest('hex');
  })
});

4. Don't Disable interpretHeader Without Understanding

// BAD - ignores server's cache directives
setupCache(axios, { interpretHeader: false });

// Server sends Cache-Control: no-store - still cached!

// GOOD - disable only for specific requests that need it
axios.get('/api/data', {
  cache: { interpretHeader: false, ttl: 60000 }
});

5. Don't Forget to Handle Vary

// BAD - disabling vary can cause cache poisoning
setupCache(axios, { vary: false });

// User A sees user B's data!

// GOOD - keep vary enabled (default)
setupCache(axios, { vary: true });

Performance Considerations

Memory Usage

  • Default maxEntries: 1024 may need tuning
  • Large response bodies consume memory quickly
  • Consider persistent storage (localStorage, Redis) for browser/server
  • maxStaleAge prevents stale entry accumulation

Network Efficiency

  • Request deduplication eliminates redundant calls
  • Stale-while-revalidate minimizes perceived latency
  • ETag/Last-Modified saves bandwidth (304 responses)
  • cacheTakeover prevents double caching (browser + library)

Computational Cost

  • Key generation: O(1) hash with object-code
  • Cache lookup: O(1) with Map storage
  • Vary comparison: O(n) where n = number of vary headers
  • Header interpretation: O(1) string parsing

Migration Guide (for Future Contributors)

From v0 to v1

  • Axios version changed: 0.x → 1.x
  • Breaking type changes
  • New per-request enabled flag
  • Deprecated cache: false in favor of cache: { enabled: false }

Deprecated Features (v1.x)

These work but will be removed in v2:

  • location option
  • waiting Map (exposed property)
  • headerInterpreter (replacing with internal)
  • requestInterceptor/responseInterceptor (replacing with internal)

Troubleshooting Guide

Cache not working

Check:

  1. Is method in cache.methods? (default: ['get', 'head'])
  2. Does status code pass cachePredicate.statusCheck?
  3. Are headers saying "don't cache"? (enable debug mode)
  4. Is URL in ignoreUrls?
  5. Is cache.enabled false?

Stale data served

Check:

  1. TTL too long?
  2. interpretHeader reading old Cache-Control max-age?
  3. Clock skew causing Expires calculation issues?
  4. override: false when you need fresh data?

Memory leak

Check:

  1. cleanupInterval disabled?
  2. maxEntries too high or disabled?
  3. Stale entries accumulating (maxStaleAge too high)?
  4. Custom storage not implementing cleanup?

Wrong data returned (cache poisoning)

Check:

  1. vary: false when server uses Vary header?
  2. Custom key generator not including all discriminating factors?
  3. Mutating cached data without cloneData?

Concurrent requests not deduplicating

Check:

  1. Are request IDs identical? (check with debug mode)
  2. Different cache.override values?
  3. One has cache: false?

Resources

Contributing Notes

When working on this codebase:

  1. Read tests first - They document behavior better than comments
  2. Enable debug mode - Essential for understanding flow
  3. Test concurrent scenarios - Many bugs only appear with Promise.all
  4. Consider HTTP specs - RFC 7231, 7232, 7234 define expected behavior
  5. Update types - TypeScript is first-class citizen
  6. Document in VitePress - User-facing changes need docs
  7. Check deprecation warnings - Some APIs being removed in v2
  8. Avoid breaking changes - Library is stable, breaking changes need major version

Quick Reference

Type Imports

import type {
  AxiosCacheInstance,
  CacheAxiosResponse,
  CacheRequestConfig,
  CacheProperties,
  StorageValue,
  AxiosStorage,
  KeyGenerator,
  HeaderInterpreter
} from 'axios-cache-interceptor';

Common Tasks

Setup:

import { setupCache } from 'axios-cache-interceptor';
const axios = setupCache(Axios.create());

Cache check:

const cache = await axios.storage.get(requestId);
console.log(cache.state); // 'empty' | 'cached' | 'stale' | 'loading'

Clear cache:

await axios.storage.remove(requestId);

Inspect request:

const response = await axios.get('/api/data');
console.log(response.cached, response.stale, response.id);

This guide should help you understand and work effectively with the axios-cache-interceptor codebase. For the most up-to-date information, always refer to the source code and tests.