cache-control-parse
v0.1.0
Published
Zero-dependency RFC 9111 Cache-Control header parser and serializer with ordering, extension support, and strict error reporting
Maintainers
Readme
cache-control-parse
Zero-dependency RFC 9111
Cache-Controlheader parser and serializer for Node.js — preserves ordering, supportsno-cache="Set-Cookie"field-name arguments, surfaces extension directives, and throws on malformed input.
Quick Start
npm install cache-control-parseimport { parseCacheControl, serializeCacheControl, classifyDirectives } from 'cache-control-parse';
// Parse
const directives = parseCacheControl('max-age=60, public, no-cache="Set-Cookie"');
// → [{ name:'max-age', kind:'delta-seconds', value:60, position:0 }, ...]
// Serialize
serializeCacheControl(directives);
// → 'max-age=60, public, no-cache="Set-Cookie"'
// Classify into request / response buckets
classifyDirectives(directives, { direction: 'request' });
// → { request: { 'max-age': 60, 'public': true, 'no-cache': ['Set-Cookie'] }, response: {} }Performance & Benchmarks
| Workload | parse (ms) | serialize (ms) | classify (ms) | |---|---|---|---| | simple | 13.98ms | 2.65ms | 3.70ms | | moderate | 64.04ms | 8.17ms | 10.95ms | | complex | 151.69ms | 16.98ms | 18.00ms | | fieldNames | 31.74ms | 6.42ms | 3.32ms | | extensions | 43.14ms | 10.21ms | 0.71ms |
50,000 iterations per workload, best of 5 runs. Node.js 3.11.15.
Replicate locally: node benchmarks/run_benchmark.js
Why cache-control-parse?
The leading npm package (cache-control-parser, etienne-martin, 124k weekly downloads) flattens parsed directives into an unordered object, silently drops unknown directives, has no support for no-cache="Set-Cookie" field-name arguments, and reports no errors on malformed input. Every Node.js HTTP cache layer (Express cache middleware, Fastify caching, CDN edge workers, AWS Lambda@Edge caching code) re-implements this parsing inline.
Trade-off decisions:
- Ordering preserved — directive order is meaningful (e.g.,
Cache-Control: max-age=0, no-cacheis different fromno-cache, max-age=0). Thepositionfield on each directive lets callers apply order-sensitive semantics. - Strict parsing — throws
InvalidCacheControlErroron malformed input; no silent fallbacks that mask configuration errors. - Extension directives surfaced — unknown directives are returned as
kind: 'extension', not dropped. - Field-name arguments —
private="X"andno-cache="Set-Cookie"are parsed askind: 'field-names'with the field names as a string array.
Key Features
- Zero dependencies — no transitive risk, tree-shakeable, works in any ESM environment
- RFC 9111 compliant — parses all standard directives, quoted strings with escapes, and extension tokens
no-cache="Field-Name"— field-name arguments parsed askind: 'field-names'arrays, not dropped- Ordering preserved —
positionindex on each directive - Error reporting —
InvalidCacheControlErrorwithheader,position, andmessageon bad input - Serializer —
serializeCacheControl(directives)→ canonical header string - Classifier —
classifyDirectives(parsed, { direction })→{ request, response } - TypeScript types —
index.d.tsincluded
API Reference
parseCacheControl(header)
Parses a Cache-Control header string. Returns an array of directive objects.
const directives = parseCacheControl('max-age=60, public, no-cache="Set-Cookie"');Each directive object:
| Property | Type | Description |
|---|---|---|
| name | string | Directive name (lowercase, original case preserved) |
| kind | 'delta-seconds' \| 'flag' \| 'field-names' \| 'extension' | Directive category |
| value | number \| boolean \| string[] \| string \| null | Parsed value |
| position | number | Zero-based index in the original header |
Kinds:
delta-seconds:max-age,s-maxage,max-stale,min-fresh,stale-while-revalidate,stale-if-error→value: numberflag:public,private,no-cache,no-store,no-transform,only-if-cached,must-revalidate,proxy-revalidate,immutable,must-understand→value: nullfield-names:private="...",no-cache="..."→value: string[]extension: everything else →value: string | null
Throws InvalidCacheControlError on malformed input (invalid token, unclosed quote, adjacent tokens without comma, non-numeric delta-seconds).
serializeCacheControl(directives)
Serializes an array of directive objects back to a canonical header string.
serializeCacheControl([
{ name: 'max-age', kind: 'delta-seconds', value: 60, position: 0 },
{ name: 'public', kind: 'flag', value: null, position: 1 },
]);
// → 'max-age=60, public'Throws InvalidCacheControlError if directives is not an array.
classifyDirectives(directives, options?)
Classifies directives into request-specific and response-specific buckets.
const { request, response } = classifyDirectives(parsed, { direction: 'auto' });Options:
| Option | Type | Default | Description |
|---|---|---|---|
| direction | 'auto' \| 'request' \| 'response' | 'auto' | Which bucket set to populate |
auto(default): shared directives (max-age,no-cache, etc.) go torequestonly; response-only go toresponseonlyrequest: populatesrequestonly; all recognized request directives includedresponse: populatesresponseonly; all recognized response directives included
Unknown extensions are skipped (they go in neither bucket; inspect the raw directives array).
InvalidCacheControlError
import { InvalidCacheControlError } from 'cache-control-parse';
try {
parseCacheControl('max-age=abc');
} catch (e) {
if (e instanceof InvalidCacheControlError) {
console.log(e.header); // 'max-age=abc'
console.log(e.position); // 0
console.log(e.message); // '...'
}
}Properties: name, message, header, position, stack.
CLI
# Parse and classify as request
node cli.mjs "max-age=60, no-cache" --classify --direction request
# JSON output
node cli.mjs "max-age=60" --json
# Classify response directives
node cli.mjs "public, s-maxage=60" --classify --direction responseLimitations
private="X"field-name argument is parsed but not validated against known response headers.- Extension directives are preserved as opaque tokens; no semantic interpretation.
s-maxagein a request header is treated as an extension (RFC 9111 says response-only).- The parser does not enforce HTTP semantics (e.g., duplicate directives are preserved per input, not deduplicated).
- HTTP freshness lifetime calculation (RFC 9111 §4.2) is out of scope — caller computes from parsed directives.
Non-Goals
- Freshness / expiry calculation (out of scope — use a separate HTTP date library)
- Header list deduplication (out of scope — caller handles multi-header
Cache-Controlvalues) - Node.js HTTP response header object coercion (out of scope — the string API is universal)
- Browser-native
Headersobject integration (out of scope — parses any string, caller does the wire format) - HTTP/2 push promises, trailers, or alternative representations (out of scope)
License
MIT — see LICENSE.
