Typed utilities for parsing, manipulating, and serializing HTTP header values. headers provides focused classes for common HTTP headers.
Features
- Enhanced
Headers Class - Use SuperHeaders for lazy, typed property accessors that still work anywhere native Headers are expected
- Header-Specific Classes - Purpose-built APIs for
Accept, Cache-Control, Content-Type, and more
- Round-Trip Safety - Parse from raw values and serialize back with
.toString()
- Typed Operations - Work with structured values instead of manual string parsing
Installation
npm i remix
Usage
SuperHeaders extends the native Headers class and adds lazy, typed property accessors for common headers. It is also the default export from this package.
import Headers from 'remix/headers'
let headers = new Headers(request.headers)
headers.contentType = { mediaType: 'text/html', charset: 'utf-8' }
headers.cacheControl = { public: true, maxAge: 3600 }
headers.setCookie = { name: 'session', value: 'abc', httpOnly: true }
headers.contentType.charset = 'iso-8859-1'
headers.cacheControl.maxAge = 60
headers.setCookie.push({ name: 'theme', value: 'dark', path: '/' })
return new Response(html, { headers })
Because SuperHeaders is a real Headers subclass, it can be passed directly to platform APIs:
let headers = new Headers({ contentType: 'text/plain' })
headers instanceof globalThis.Headers
new Response('Hello', { headers }).headers.get('Content-Type')
Typed accessors parse values only when you read them:
let headers = new Headers({ 'Content-Type': 'application/json; charset=utf-8' })
headers.get('Content-Type')
headers.contentType.mediaType
Use apply() to apply a SuperHeadersInit value to an existing instance with header-aware semantics:
let headers = new Headers({
contentType: 'text/html',
setCookie: { name: 'session', value: 'abc' },
vary: 'Accept-Encoding',
})
headers.apply({
contentType: 'application/json',
setCookie: { name: 'theme', value: 'dark' },
vary: ['Accept-Encoding', 'Accept-Language'],
})
headers.get('Content-Type')
headers.get('Vary')
headers.getSetCookie()
Each supported header has a class that represents the header value. Use the static from() method to parse header values. Each class has a toString() method that returns the header value as a string, which you can either call manually, or will be called automatically when the header class is used in a context that expects a string.
The following headers are currently supported:
If you only need a specific header parser (for example, just Content-Type), import that parser directly from its subpath. This avoids pulling the package barrel and SuperHeaders:
import { ContentType } from 'remix/headers/content-type'
import { SetCookie } from 'remix/headers/set-cookie'
let contentType = ContentType.from('text/plain; charset=utf-8')
let setCookie = new SetCookie('session=abc; Path=/')
Accept
Parse, manipulate and stringify Accept headers.
Implements Map<mediaType, quality>.
import { Accept } from 'remix/headers'
let accept = Accept.from(request.headers.get('Accept'))
accept.mediaTypes
accept.weights
accept.accepts('text/html')
accept.accepts('text/plain')
accept.accepts('image/jpeg')
accept.getWeight('text/plain')
accept.getPreferred(['text/html', 'text/plain'])
for (let [mediaType, quality] of accept) {
}
accept.set('application/json', 0.8)
accept.delete('text/*')
headers.set('Accept', accept.toString())
new Accept('text/html, text/*;q=0.9')
new Accept({ 'text/html': 1, 'text/*': 0.9 })
new Accept(['text/html', ['text/*', 0.9]])
let requestHeaders = new Headers({
Accept: new Accept({ 'text/html': 1, 'application/json': 0.8 }).toString(),
})
requestHeaders.set('Accept', new Accept({ 'text/html': 1, 'application/json': 0.8 }).toString())
Accept-Encoding
Parse, manipulate and stringify Accept-Encoding headers.
Implements Map<encoding, quality>.
import { AcceptEncoding } from 'remix/headers'
let acceptEncoding = AcceptEncoding.from(request.headers.get('Accept-Encoding'))
acceptEncoding.encodings
acceptEncoding.weights
acceptEncoding.accepts('gzip')
acceptEncoding.accepts('br')
acceptEncoding.getWeight('gzip')
acceptEncoding.getPreferred(['gzip', 'deflate', 'br'])
acceptEncoding.set('br', 1)
acceptEncoding.delete('deflate')
headers.set('Accept-Encoding', acceptEncoding.toString())
new AcceptEncoding('gzip, deflate;q=0.8')
new AcceptEncoding({ gzip: 1, deflate: 0.8 })
let requestHeaders = new Headers({
'Accept-Encoding': new AcceptEncoding({ gzip: 1, br: 0.9 }).toString(),
})
requestHeaders.set('Accept-Encoding', new AcceptEncoding({ gzip: 1, br: 0.9 }).toString())
Accept-Language
Parse, manipulate and stringify Accept-Language headers.
Implements Map<language, quality>.
import { AcceptLanguage } from 'remix/headers'
let acceptLanguage = AcceptLanguage.from(request.headers.get('Accept-Language'))
acceptLanguage.languages
acceptLanguage.weights
acceptLanguage.accepts('en-US')
acceptLanguage.accepts('en-GB')
acceptLanguage.getWeight('en-GB')
acceptLanguage.getPreferred(['en-US', 'en-GB', 'fr'])
acceptLanguage.set('fr', 0.5)
acceptLanguage.delete('en')
headers.set('Accept-Language', acceptLanguage.toString())
new AcceptLanguage('en-US, en;q=0.9')
new AcceptLanguage({ 'en-US': 1, en: 0.9 })
let requestHeaders = new Headers({
'Accept-Language': new AcceptLanguage({ 'en-US': 1, fr: 0.5 }).toString(),
})
requestHeaders.set('Accept-Language', new AcceptLanguage({ 'en-US': 1, fr: 0.5 }).toString())
Cache-Control
Parse, manipulate and stringify Cache-Control headers.
import { CacheControl } from 'remix/headers'
let cacheControl = CacheControl.from(response.headers.get('Cache-Control'))
cacheControl.public
cacheControl.maxAge
cacheControl.sMaxage
cacheControl.noCache
cacheControl.noStore
cacheControl.noTransform
cacheControl.mustRevalidate
cacheControl.immutable
cacheControl.maxAge = 7200
cacheControl.immutable = true
headers.set('Cache-Control', cacheControl.toString())
new CacheControl('public, max-age=3600')
new CacheControl({ public: true, maxAge: 3600 })
let responseHeaders = new Headers({
'Cache-Control': new CacheControl({ public: true, maxAge: 3600 }).toString(),
})
responseHeaders.set('Cache-Control', new CacheControl({ public: true, maxAge: 3600 }).toString())
Content-Disposition
Parse, manipulate and stringify Content-Disposition headers.
import { ContentDisposition } from 'remix/headers'
let contentDisposition = ContentDisposition.from(response.headers.get('Content-Disposition'))
contentDisposition.type
contentDisposition.filename
contentDisposition.filenameSplat
contentDisposition.preferredFilename
contentDisposition.filename = 'download.pdf'
headers.set('Content-Disposition', contentDisposition.toString())
new ContentDisposition('attachment; filename="example.pdf"')
new ContentDisposition({ type: 'attachment', filename: 'example.pdf' })
let responseHeaders = new Headers({
'Content-Disposition': new ContentDisposition({
type: 'attachment',
filename: 'example.pdf',
}).toString(),
})
responseHeaders.set(
'Content-Disposition',
new ContentDisposition({ type: 'attachment', filename: 'example.pdf' }).toString(),
)
Filename Safety
Content-Disposition filenames received from clients are untrusted input. filename and filenameSplat expose parsed parameter values; preferredFilename selects and decodes filename* when available, falling back to filename. These APIs preserve filename metadata without sanitizing it for filesystem use, so values can contain path separators, .. segments, or absolute paths.
Do not use any of these values, or a File.name derived from them, directly as a filesystem path or join them to an upload directory. Generate a storage name in your application, such as crypto.randomUUID(), within a directory you control, and keep the submitted filename only as metadata. The same rule applies to filenames received from remote servers. See RFC 6266, section 4.3 and RFC 7578, section 4.2.
Content-Range
Parse, manipulate and stringify Content-Range headers.
import { ContentRange } from 'remix/headers'
let contentRange = ContentRange.from(response.headers.get('Content-Range'))
contentRange.unit
contentRange.start
contentRange.end
contentRange.size
let unsatisfied = ContentRange.from('bytes */67589')
unsatisfied.start
unsatisfied.end
unsatisfied.size
new ContentRange({ unit: 'bytes', start: 0, end: 499, size: 1000 })
let headers = new Headers({
'Content-Range': new ContentRange({ unit: 'bytes', start: 0, end: 499, size: 1000 }).toString(),
})
headers.set(
'Content-Range',
new ContentRange({ unit: 'bytes', start: 0, end: 499, size: 1000 }).toString(),
)
Content-Type
Parse, manipulate and stringify Content-Type headers.
import { ContentType } from 'remix/headers'
let contentType = ContentType.from(request.headers.get('Content-Type'))
contentType.mediaType
contentType.charset
contentType.boundary
contentType.charset = 'iso-8859-1'
headers.set('Content-Type', contentType.toString())
new ContentType('text/html; charset=utf-8')
new ContentType({ mediaType: 'text/html', charset: 'utf-8' })
let responseHeaders = new Headers({
'Content-Type': new ContentType({ mediaType: 'text/html', charset: 'utf-8' }).toString(),
})
responseHeaders.set(
'Content-Type',
new ContentType({ mediaType: 'text/html', charset: 'utf-8' }).toString(),
)
Cookie
Parse, manipulate and stringify Cookie headers.
Implements an ordered list of name/value pairs. Duplicate cookie names are preserved, such as when cookies with the same name were set for different paths.
import { Cookie } from 'remix/headers'
let cookie = Cookie.from(request.headers.get('Cookie'))
cookie.get('session_id')
cookie.getAll('session_id')
cookie.get('theme')
cookie.has('session_id')
cookie.size
for (let [name, value] of cookie) {
}
cookie.set('theme', 'light')
cookie.append('session_id', 'def456')
cookie.delete('session_id')
headers.set('Cookie', cookie.toString())
new Cookie('session_id=abc123; theme=dark')
new Cookie({ session_id: 'abc123', theme: 'dark' })
new Cookie([
['session_id', 'abc123'],
['theme', 'dark'],
])
let requestHeaders = new Headers({
Cookie: new Cookie({ session_id: 'abc123', theme: 'dark' }).toString(),
})
requestHeaders.set('Cookie', new Cookie({ session_id: 'abc123', theme: 'dark' }).toString())
If-Match
Parse, manipulate and stringify If-Match headers.
import { IfMatch } from 'remix/headers'
let ifMatch = IfMatch.from(request.headers.get('If-Match'))
ifMatch.tags
ifMatch.has('"67ab43"')
ifMatch.matches('"67ab43"')
ifMatch.matches('"abc123"')
let weak = IfMatch.from('W/"67ab43"')
weak.matches('W/"67ab43"')
ifMatch.tags.push('"newetag"')
ifMatch.tags = ifMatch.tags.filter((tag) => tag !== '"67ab43"')
headers.set('If-Match', ifMatch.toString())
new IfMatch(['abc123', 'def456'])
let requestHeaders = new Headers({
'If-Match': new IfMatch(['"abc123"', '"def456"']).toString(),
})
requestHeaders.set('If-Match', new IfMatch(['"abc123"', '"def456"']).toString())
If-None-Match
Parse, manipulate and stringify If-None-Match headers.
import { IfNoneMatch } from 'remix/headers'
let ifNoneMatch = IfNoneMatch.from(request.headers.get('If-None-Match'))
ifNoneMatch.tags
ifNoneMatch.has('"67ab43"')
ifNoneMatch.matches('"67ab43"')
let weak = IfNoneMatch.from('W/"67ab43"')
weak.matches('W/"67ab43"')
ifNoneMatch.tags.push('"newetag"')
ifNoneMatch.tags = ifNoneMatch.tags.filter((tag) => tag !== '"67ab43"')
headers.set('If-None-Match', ifNoneMatch.toString())
new IfNoneMatch(['abc123'])
let requestHeaders = new Headers({
'If-None-Match': new IfNoneMatch(['"abc123"']).toString(),
})
requestHeaders.set('If-None-Match', new IfNoneMatch(['"abc123"']).toString())
If-Range
Parse, manipulate and stringify If-Range headers.
import { IfRange } from 'remix/headers'
let ifRange = IfRange.from(request.headers.get('If-Range'))
ifRange.matches({ lastModified: 1609459200000 })
ifRange.matches({ lastModified: new Date('2021-01-01') })
let etagHeader = IfRange.from('"67ab43"')
etagHeader.matches({ etag: '"67ab43"' })
let absent = IfRange.from(null)
absent.matches({ etag: '"any"' })
let empty = IfRange.from('')
empty.matches({ etag: '"any"' })
new IfRange('"abc123"')
let headers = new Headers({
'If-Range': new IfRange('"abc123"').toString(),
})
headers.set('If-Range', new IfRange('"abc123"').toString())
Range
Parse, manipulate and stringify Range headers.
import { Range } from 'remix/headers'
let range = Range.from(request.headers.get('Range'))
range.unit
range.ranges
range.canSatisfy(2000)
range.canSatisfy(500)
range.normalize(2000)
let multi = Range.from('bytes=0-499, 1000-1499')
multi.ranges.length
let suffix = Range.from('bytes=-500')
suffix.normalize(2000)
new Range({ unit: 'bytes', ranges: [{ start: 0, end: 999 }] })
let headers = new Headers({
Range: new Range({ unit: 'bytes', ranges: [{ start: 0, end: 999 }] }).toString(),
})
headers.set('Range', new Range({ unit: 'bytes', ranges: [{ start: 0, end: 999 }] }).toString())
Set-Cookie
Parse, manipulate and stringify Set-Cookie headers.
import { SetCookie } from 'remix/headers'
let setCookie = SetCookie.from(response.headers.get('Set-Cookie'))
setCookie.name
setCookie.value
setCookie.path
setCookie.httpOnly
setCookie.secure
setCookie.domain
setCookie.maxAge
setCookie.expires
setCookie.sameSite
setCookie.maxAge = 3600
setCookie.sameSite = 'Strict'
headers.set('Set-Cookie', setCookie.toString())
new SetCookie('session_id=abc; Path=/; HttpOnly; Secure')
new SetCookie({
name: 'session_id',
value: 'abc',
path: '/',
httpOnly: true,
secure: true,
})
let responseHeaders = new Headers({
'Set-Cookie': new SetCookie({ name: 'session_id', value: 'abc', httpOnly: true }).toString(),
})
responseHeaders.set(
'Set-Cookie',
new SetCookie({ name: 'session_id', value: 'abc', httpOnly: true }).toString(),
)
Vary
Parse, manipulate and stringify Vary headers.
Implements Set<headerName>.
import { Vary } from 'remix/headers'
let vary = Vary.from(response.headers.get('Vary'))
vary.headerNames
vary.has('Accept-Encoding')
vary.size
vary.add('User-Agent')
vary.delete('Accept-Language')
headers.set('Vary', vary.toString())
new Vary('Accept-Encoding, Accept-Language')
new Vary(['Accept-Encoding', 'Accept-Language'])
new Vary({ headerNames: ['Accept-Encoding', 'Accept-Language'] })
let responseHeaders = new Headers({
Vary: new Vary(['Accept-Encoding', 'Accept-Language']).toString(),
})
responseHeaders.set('Vary', new Vary(['Accept-Encoding', 'Accept-Language']).toString())
Parse and stringify raw HTTP header strings.
import { parse, stringify } from 'remix/headers'
let headers = parse('Content-Type: text/html\r\nCache-Control: no-cache')
headers.get('Content-Type')
headers.get('Cache-Control')
stringify(headers)
Related Packages
License
See LICENSE