This is the changelog for headers. It follows semantic versioning.
- Expose parser-specific header modules from
@remix-run/headersvia dedicated subpath exports and migrate internal header-parser consumers (including multipart parser) to import from those subpaths.
- Add
SuperHeaders#apply(init)to applySuperHeadersInitvalues to an existing instance with header-aware behavior, replacing singleton headers while preserving additive headers likeCookie,Set-Cookie, andVary(see #11398).
-
Add explicit public API return types to header value serialization methods so generated declarations no longer depend on inferred method signatures (see #11433).
-
Fix
CookieandSuperHeaders.cookieso duplicate cookie names from path- or domain-specific cookies are preserved in order.Cookie#get(name)now returns the first matching value,Cookie#getAll(name)can be used to read every matching value, andCookie#append(name, value)can be used to add another cookie with the same name (see #11423).
-
Added
SuperHeadersas the default and named export from@remix-run/headers.SuperHeadersextends the nativeHeadersclass and restores lazy, typed property accessors while keeping nativeHeadersstorage synchronized for platform APIs likeResponse.This restores the property accessor behavior that existed before it was removed in #10911.
Supported accessors include
Accept,Accept-Charset,Accept-Encoding,Accept-Language,Accept-Patch,Accept-Post,Accept-Ranges,Access-Control-Allow-Credentials,Access-Control-Allow-Headers,Access-Control-Allow-Methods,Access-Control-Allow-Origin,Access-Control-Expose-Headers,Access-Control-Max-Age,Access-Control-Request-Headers,Access-Control-Request-Method,Age,Allow,Authorization,Cache-Control,Connection,Content-Disposition,Content-Encoding,Content-Language,Content-Length,Content-Location,Content-Range,Content-Security-Policy,Content-Security-Policy-Report-Only,Content-Type,Cookie,Cross-Origin-Embedder-Policy,Cross-Origin-Embedder-Policy-Report-Only,Cross-Origin-Opener-Policy,Cross-Origin-Opener-Policy-Report-Only,Cross-Origin-Resource-Policy,Date,ETag,Expect,Expires,Forwarded,From,Host,Idempotency-Key,If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since,Keep-Alive,Last-Modified,Link,Location,Max-Forwards,Origin,Permissions-Policy,Pragma,Prefer,Preference-Applied,Range,Referer,Referrer-Policy,Refresh,Retry-After,Server,Set-Cookie,Strict-Transport-Security,Traceparent,Tracestate,Upgrade-Insecure-Requests,User-Agent,Vary,Via,WWW-Authenticate,X-Content-Type-Options,X-Forwarded-For,X-Forwarded-Host,X-Forwarded-Proto,X-Frame-Options,X-Powered-By, andX-Robots-Tag.
- Preserve literal
+characters when decoding RFC 8187filename*values forContentDisposition.preferredFilename.
-
BREAKING CHANGE: Removed
Headers/SuperHeadersclass and default export. Use the nativeHeadersclass with the staticfrom()method on each header class instead.New individual header
.from()methods:Accept.from()AcceptEncoding.from()AcceptLanguage.from()CacheControl.from()ContentDisposition.from()ContentRange.from()ContentType.from()Cookie.from()IfMatch.from()IfNoneMatch.from()IfRange.from()Range.from()SetCookie.from()Vary.from()
New raw header utilities added:
parse()stringify()
Migration example:
// Before: import SuperHeaders from '@remix-run/headers' let headers = new SuperHeaders(request.headers) let mediaType = headers.contentType.mediaType // After: import { ContentType } from '@remix-run/headers' let contentType = ContentType.from(request.headers.get('Content-Type')) let mediaType = contentType.mediaType
If you were using the
Headersconstructor to parse raw HTTP header strings, useparse()instead:// Before: import SuperHeaders from '@remix-run/headers' let headers = new SuperHeaders('Content-Type: text/html\r\nCache-Control: no-cache') // After: import { parse } from '@remix-run/headers' let headers = parse('Content-Type: text/html\r\nCache-Control: no-cache')
If you were using
headers.toString()to convert headers to raw format, usestringify()instead:// Before: import SuperHeaders from '@remix-run/headers' let headers = new SuperHeaders() headers.set('Content-Type', 'text/html') let rawHeaders = headers.toString() // After: import { stringify } from '@remix-run/headers' let headers = new Headers() headers.set('Content-Type', 'text/html') let rawHeaders = stringify(headers)
- Add
Varysupport
import { Vary } from '@remix-run/headers'
let header = new Vary('Accept-Encoding')
header.add('Accept-Language')
header.headerNames // ['accept-encoding', 'accept-language']
header.toString() // 'accept-encoding, accept-language'Accept.getPreferred(),AcceptEncoding.getPreferred(), andAcceptLanguage.getPreferred()are now generic, preserving the union type of the input array in the return type
- Fix
secureproperty type inSetCookieto acceptbooleaninstead of onlytrue, making it consistent withhttpOnlyandpartitioned
- Fix bug where
Max-Age=0did not show up inSetCookieheader
- Add
Rangesupport
import { Range } from '@remix-run/headers'
let header = new Range({ unit: 'bytes', ranges: [{ start: 0, end: 999 }] })
header.toString() // "bytes=0-999"
// Parse from string
let header = new Range('bytes=0-999,2000-2999')
header.ranges // [{ start: 0, end: 999 }, { start: 2000, end: 2999 }]
// Check if range is satisfiable for a given file size
header.isSatisfiable(5000) // true
// Normalize ranges to concrete start/end values for a given file size
let header = new Range('bytes=0-')
header.normalize(5000) // [{ start: 0, end: 4999 }]- Add
Content-Rangesupport
import { ContentRange } from '@remix-run/headers'
let header = new ContentRange({
unit: 'bytes',
start: 0,
end: 999,
size: 5000,
})
header.toString() // "bytes 0-999/5000"
// Parse from string
let header = new ContentRange('bytes 200-1000/67589')
header.start // 200
header.end // 1000
header.size // 67589- Add
If-Matchsupport
import { IfMatch } from '@remix-run/headers'
let header = new IfMatch(['"abc123"', '"def456"'])
header.has('"abc123"') // true
// Check if precondition passes
header.matches('"abc123"') // true
header.matches('"xyz789"') // false
header.matches('W/"abc123"') // false (weak ETags never match)- Add
If-Rangesupport
import { IfRange } from '@remix-run/headers'
// With ETag
let header = new IfRange('"abc123"')
header.matches({ etag: '"abc123"' }) // true
header.matches({ etag: 'W/"abc123"' }) // false (weak ETags never match)
// With Last-Modified date
let header = new IfRange(new Date('2025-10-21T07:28:00Z'))
header.matches({ lastModified: new Date('2025-10-21T07:28:00Z') }) // true- Add
Allowsupport
import { SuperHeaders } from '@remix-run/headers'
let headers = new SuperHeaders({ allow: ['GET', 'POST', 'OPTIONS'] })
headers.get('Allow') // "GET, POST, OPTIONS"- Build using
tscinstead ofesbuild. This means modules in thedistdirectory now mirror the layout of modules in thesrcdirectory.
- Add support for
httpOnly: falseinSetCookieconstructor - Export
CookiePropertiestype with all cookie properties - Add
Partitionedsupport toSetCookie
- BREAKING CHANGE: Removed CommonJS build. This package is now ESM-only. If you need to use this package in a CommonJS project, you will need to use dynamic
import().
- Drop support for TypeScript < 5.7
- Rename package from
@mjackson/headersto@remix-run/headers
- Do not minify builds
- Remove some test files from the build
- Add
/srcto npm package, so "go to definition" goes to the actual source - Use one set of types for all built files, instead of separate types for ESM and CJS
- Build using esbuild directly instead of tsup
This release contains several improvements to Cookie that bring it more in line with other headers like Accept, AcceptEncoding, and AcceptLanguage.
- BREAKING CHANGE:
cookie.names()andcookie.values()are now getters that returnstring[]instead of methods that returnIterableIterator<string> - BREAKING CHANGE:
cookie.forEach()calls its callback with(name, value, cookie)instead of(value, name, map) - BREAKING CHANGE:
cookie.delete(name)returnsvoidinstead ofboolean
// before
let cookieNames = Array.from(headers.cookie.names())
// after
let cookieNames = headers.cookie.namesAdditionally, this release adds support for the If-None-Match header. This is useful for conditional GET requests where you want to return a response with content only if the ETag has changed.
import { SuperHeaders } from '@remix-run/headers'
function requestHandler(request: Request): Promise<Response> {
let response = await callDownstreamService(request)
if (request.method === 'GET' && response.headers.has('ETag')) {
let headers = new SuperHeaders(request.headers)
if (headers.ifNoneMatch.matches(response.headers.get('ETag'))) {
return new Response(null, { status: 304 })
}
}
return response
}This release tightens up the type safety and brings SuperHeaders more in line with the built-in Headers interface.
- BREAKING CHANGE: The mutation methods
headers.set()andheaders.append()no longer accept anything other than a string as the 2nd arg. This follows the nativeHeadersinterface more closely.
// before
let headers = new SuperHeaders()
headers.set('Content-Type', { mediaType: 'text/html' })
// after
headers.set('Content-Type', 'text/html')
// if you need the previous behavior, use the setter instead of set()
headers.contentType = { mediaType: 'text/html' }Similarly, the constructor no longer accepts non-string values in an array init value.
// before
let headers = new SuperHeaders([['Content-Type', { mediaType: 'text/html' }]])
// if you need the previous behavior, use the object init instead
let headers = new SuperHeaders({ contentType: { mediaType: 'text/html' } })- BREAKING CHANGE:
headers.get()returnsnullfor uninitialized custom header values instead ofundefined. This follows the nativeHeadersinterface more closely.
// before
let headers = new SuperHeaders()
headers.get('Host') // null
headers.get('Content-Type') // undefined
// after
headers.get('Host') // null
headers.get('Content-Type') // null- BREAKING CHANGE: Removed ability to initialize
AcceptLanguagewithundefinedweight values.
// before
let h1 = new AcceptLanguage({ 'en-US': undefined })
let h2 = new AcceptLanguage([['en-US', undefined]])
// after
let h3 = new AcceptLanguage({ 'en-US': 1 })- All setters now also accept
undefined | nullin addition tostringand custom object values. Setting a header toundefined | nullis the same as usingheaders.delete().
let headers = new SuperHeaders({ contentType: 'text/html' })
headers.get('Content-Type') // 'text/html'
headers.contentType = null // same as headers.delete('Content-Type');
headers.get('Content-Type') // null- Allow setting date headers (
date,expires,ifModifiedSince,ifUnmodifiedSince, andlastModified) using numbers.
let ms = new Date().getTime()
let headers = new SuperHeaders({ lastModified: ms })
headers.date = ms- Added
AcceptLanguage.prototype.accepts(language),AcceptLanguage.prototype.getWeight(language),AcceptLanguage.prototype.getPreferred(languages)
import { AcceptLanguage } from '@remix-run/headers'
let header = new AcceptLanguage({ 'en-US': 1, en: 0.9 })
header.accepts('en-US') // true
header.accepts('en-GB') // true
header.accepts('en') // true
header.accepts('fr') // false
header.getWeight('en-US') // 1
header.getWeight('en-GB') // 0.9
header.getPreferred(['en-GB', 'en-US']) // 'en-US'- Added
Acceptsupport
import { Accept } from '@remix-run/headers'
let header = new Accept({ 'text/html': 1, 'text/*': 0.9 })
header.accepts('text/html') // true
header.accepts('text/plain') // true
header.accepts('text/*') // true
header.accepts('image/jpeg') // false
header.getWeight('text/html') // 1
header.getWeight('text/plain') // 0.9
header.getPreferred(['text/html', 'text/plain']) // 'text/html'- Added
Accept-Encodingsupport
import { AcceptEncoding } from '@remix-run/headers'
let header = new AcceptEncoding({ gzip: 1, deflate: 0.9 })
header.accepts('gzip') // true
header.accepts('deflate') // true
header.accepts('identity') // true
header.accepts('br') // false
header.getWeight('gzip') // 1
header.getWeight('deflate') // 0.9
header.getPreferred(['gzip', 'deflate']) // 'gzip'- Added
SuperHeaders.prototype(getters and setters) for:acceptacceptEncodingacceptRangesconnectioncontentEncodingcontentLanguageetaghostlocationreferer
- Added CommonJS build
- Treat
Headersas iterable in the constructor
- Added
stringinit type tonew Headers({ acceptLanguage })
- Added support for the
Accept-Languageheader (#8, thanks @ArnoSaine)
- Associate
CacheControldoc comments with the class instead of the constructor function
- Added support for
Cache-Controlheader (mjackson/remix-the-web#7, thanks @alexanderson1993)
- Added
CookieInitsupport toheaders.cookie=setter
- Added the ability to initialize a
SuperHeadersinstance with object config instead of just strings or header object instances.
let headers = new Headers({
contentType: { mediaType: 'text/html' },
cookies: [
['session_id', 'abc'],
['theme', 'dark'],
],
})- Changed package name from
fetch-super-headersto@remix-run/headers. Eventual goal is to get theheadersnpm package name.