npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

etagparse

v0.1.0

Published

Zero-dependency HTTP ETag and cache-validation header parser, builder, and comparator for Node.js

Readme

etagparse

Zero-dependency HTTP ETag and cache-validation header parser, builder, and comparator for Node.js

"No more startsWith('W/"') hacks — parse, build, and compare RFC 9110 ETag values, If-Match / If-None-Match / If-Range lists, and Cache-Control directives with a single library."

Why etagparse?

The Node.js ecosystem has the etag package (jshttp/etag) for generating ETags from bodies, and fresh (jshttp/fresh) for HTTP freshness testing, but no zero-dependency npm package exists for parsing ETag values. Developers resort to manual string manipulation (startsWith('W/"'), slice(2, -1)) which misses edge cases: weak validators, wildcard *, RFC 9110 comparison semantics, and empty values.

etagparse is the missing primitive — the standalone parser that the Node.js stdlib does not provide.

| Edge case | etagparse | naive startsWith('W/"') | |---|---|---| | "abc" (strong) | ✅ | ✅ | | W/"abc" (weak) | ✅ | ⚠️ (needs slice(2, -1)) | | w/"abc" (lowercase) | ✅ | ❌ | | * (wildcard) | ✅ | ❌ | | "" (empty) | ✅ | ❌ | | W/ (malformed) | ✅ returns null | ❌ throws or false-positive | | "unterminated | ✅ returns null | ❌ | | RFC 9110 §13 strong vs weak comparison | ✅ | ❌ (commonly equal-by-value) |

Quick Start

npm install etagparse
const { parseETag, buildETag, etagEquals, parseIfMatch, parseCacheDirectives } = require('etagparse');

// Parse any ETag value
const weak = parseETag('W/"abc123"');
// → { strong: false, value: 'abc123', raw: 'W/"abc123"' }

const strong = parseETag('"abc123"');
// → { strong: true,  value: 'abc123', raw: '"abc123"' }

// Build an ETag
buildETag('hash', { weak: false }); // → '"hash"'
buildETag('hash', { weak: true  }); // → 'W/"hash"'

// Compare per RFC 9110
etagEquals('"abc"', '"abc"');                    // → true (strong comparison by default)
etagEquals('W/"abc"', '"abc"', { weak: true });  // → true  (weak comparison: value-only)
etagEquals('W/"abc"', '"abc"', { weak: false }); // → false (strong: weak ≠ strong)

// Parse If-Match / If-None-Match lists
parseIfMatch('"a", "b", W/"c"');     // → [ {strong,value:'a'}, {strong,value:'b'}, {strong:false,value:'c'} ]
parseIfMatch('*');                   // → { wildcard: true }
parseIfNoneMatch('"abc"');           // → [ {strong,value:'abc'} ]

// Parse If-Range (ETag OR HTTP-date)
parseIfRange('"abc"');                       // → { type:'etag', etag:{...} }
parseIfRange('Wed, 21 Oct 2015 07:28:00 GMT');// → { type:'date', date:Date(...) }
parseIfRange('not-a-date');                  // → { type:'invalid' }

// Parse Cache-Control directives
parseCacheDirectives('max-age=3600, no-cache');
// → { 'max-age': 3600, 'no-cache': true }

// Check freshness (mirrors [email protected] algorithm)
checkFreshness(
  { etag: '"abc"', lastModified: new Date('2015-10-21') },
  { 'if-none-match': '"abc"', 'if-modified-since': 'Wed, 21 Oct 2015 07:28:00 GMT' }
);
// → 'fresh'

API Reference

parseETag(input: string): ParsedETag | WildcardETag | null

Parses a single ETag header value. Trims surrounding whitespace. Returns:

  • { strong: true, value, raw } for "..."
  • { strong: false, value, raw } for W/"..." or w/"..."
  • { wildcard: true } for *
  • null for empty or malformed input
parseETag('"abc"')         // → { strong:true,  value:'abc', raw:'"abc"' }
parseETag('W/"abc"')       // → { strong:false, value:'abc', raw:'W/"abc"' }
parseETag('*')             // → { wildcard:true }
parseETag('')              // → null (does not throw)
parseETag('W/')            // → null (malformed)
parseETag('"unterminated') // → null (malformed)

buildETag(value: string, opts?: { weak?: boolean }): string

Builds an ETag header value. Default is weak (W/"..."); pass { weak: false } for strong.

buildETag('hash')                  // → 'W/"hash"'
buildETag('hash', { weak: false }) // → '"hash"'
buildETag('hash', { weak: true  }) // → 'W/"hash"'
buildETag('a"b')                   // → 'W/"a\\"b"'  (embedded quotes escaped)

Throws TypeError if value is not a string.

buildETagFromBody(body: string | Buffer): string

Builds a weak ETag by MD5-hashing the body — matches [email protected]'s weak default.

buildETagFromBody('hello')            // → 'W/"5d41402abc4b2a76b9719d911017c592"'
buildETagFromBody(Buffer.from('hi'))  // → 'W/"49f68a5c8493ec2c0bf489821c21fc3b"'

etagEquals(a, b, opts?: { weak?: boolean }): boolean

Compares two ETag values per RFC 9110 §13:

  • Default (weak: true): weak comparison — opaque-tag values match case-sensitively, weak flag ignored. W/"v""v" (same value).
  • Strong (weak: false): byte-exact match of the full header value, including weak flag. W/"v""v".
  • * always matches.
etagEquals('"abc"', '"abc"')                   // → true
etagEquals('W/"abc"', 'W/"abc"')               // → true (weak match)
etagEquals('W/"abc"', '"abc"', { weak: false })// → false (strong)
etagEquals('*', '"anything"')                  // → true (wildcard)
etagEquals('ABC', 'abc')                       // → false (case-sensitive)

parseIfMatch(header: string): ParsedETag[] | { wildcard: true } | null

Parses a comma-separated If-Match list. Handles commas inside quoted strings. Returns null for malformed.

parseIfMatch('"a", "b", W/"c"')  // → 3-element array
parseIfMatch('*')               // → { wildcard:true }
parseIfMatch('')                // → null

parseIfNoneMatch(header: string): ParsedETag[] | { wildcard: true } | null

Same shape as parseIfMatch. The header field name is the only difference.

parseIfRange(header: string): IfRangeResult | null

Detects ETag vs HTTP-date form:

parseIfRange('"abc"')                        // → { type:'etag', etag:{...} }
parseIfRange('W/"abc"')                      // → { type:'etag', etag:{...} }
parseIfRange('Wed, 21 Oct 2015 07:28:00 GMT')// → { type:'date', date:Date }
parseIfRange('not-a-date')                   // → { type:'invalid' }

parseCacheDirectives(header: string): Record<string, number | true | string>

Parses a Cache-Control header value into a directives object. Integer-valued directives (max-age=3600) become numbers; flag directives (no-cache) become true. Quoted values are unquoted and parsed if numeric.

parseCacheDirectives('max-age=3600, s-maxage=7200, no-cache, no-store')
// → { 'max-age':3600, 's-maxage':7200, 'no-cache':true, 'no-store':true }

parseCacheDirectives('private="cookie, auth"')
// → { 'private': 'cookie, auth' }

parseCacheDirectives('')  // → {}

checkFreshness(response, reqHeaders): 'fresh' | 'stale' | undefined

Mirrors the algorithm in jshttp/[email protected]:

  • If response has an etag and request has If-None-Match: compare (weak by default). * always → fresh. Match → fresh; mismatch → stale.
  • Else, if response has lastModified and request has If-Modified-Since: compare at 1-second resolution. lm <= ims → fresh; otherwise stale.
  • Returns undefined when neither check applies.
checkFreshness(
  { etag: '"a"', lastModified: new Date('2015-10-21') },
  { 'if-none-match': '"a"' }
);
// → 'fresh'

checkFreshness(
  { etag: '"x"' },
  { 'if-none-match': '"a", "b"' }
);
// → 'stale'

Install

npm install etagparse

Or from source:

git clone <repo>
cd etagparse
npm install   # no deps to install — there are none
npm test

ESM / CJS

etagparse is shipped as CommonJS. Both require('etagparse') and dynamic import('etagparse') work.

TypeScript

Type definitions are bundled at src/index.d.ts (referenced from package.json#types). All public functions and result types are exported.

Limitations

  • parseIfRange date parsing uses the JavaScript Date constructor, which is lenient. RFC 9110 mandates strict IMF-fixdate or rfc850-date or asctime-date parsing. etagparse accepts any input Date.parse recognizes.
  • opaque-tag validation only checks RFC 9110 §5.3 quoted-string syntax ("..." with \ escapes); it does NOT validate the opaque-tag payload (e.g., it cannot tell a valid base64 etag from a garbage one — by design, ETag values are opaque).
  • checkFreshness does not implement the full RFC 9110 §13 cache algorithm — it covers the ETag + If-None-Match + Last-Modified + If-Modified-Since subset. Age/Expires/max-age-based freshness is out of scope.
  • No signature verification. etagparse parses — it does not authenticate or authorize.

Non-goals

  • HTTP caching policy implementation (age, expiry, max-stale, etc.)
  • HTTP Range request handling beyond If-Range parsing
  • ETag signature verification
  • Generating ETags from file metadata (mtime + size); only buildETagFromBody (MD5 of body) is included

Tests

npm test

The test suite has 123 tests covering: every acceptance criterion, RFC 9110 strong/weak comparison, wildcard semantics, all 5 header forms, Cache-Control directive parsing (integer / flag / quoted / unquoted), empty / malformed / whitespace / unicode / very-long inputs, ESM/CJS dual export, TypeScript declarations, freshness algorithm parity, idempotent build round-trip.

License

MIT

References