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
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:
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)
new Accept('text/html, text/*;q=0.9')
new Accept({ 'text/html': 1, 'text/*': 0.9 })
new Accept(['text/html', ['text/*', 0.9]])
let headers = new Headers({
Accept: new Accept({ 'text/html': 1, 'application/json': 0.8 }),
})
headers.set('Accept', new Accept({ 'text/html': 1, 'application/json': 0.8 }))
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)
new AcceptEncoding('gzip, deflate;q=0.8')
new AcceptEncoding({ gzip: 1, deflate: 0.8 })
let headers = new Headers({
'Accept-Encoding': new AcceptEncoding({ gzip: 1, br: 0.9 }),
})
headers.set('Accept-Encoding', new AcceptEncoding({ gzip: 1, br: 0.9 }))
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)
new AcceptLanguage('en-US, en;q=0.9')
new AcceptLanguage({ 'en-US': 1, en: 0.9 })
let headers = new Headers({
'Accept-Language': new AcceptLanguage({ 'en-US': 1, fr: 0.5 }),
})
headers.set('Accept-Language', new AcceptLanguage({ 'en-US': 1, fr: 0.5 }))
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)
new CacheControl('public, max-age=3600')
new CacheControl({ public: true, maxAge: 3600 })
let headers = new Headers({
'Cache-Control': new CacheControl({ public: true, maxAge: 3600 }),
})
headers.set('Cache-Control', new CacheControl({ public: true, maxAge: 3600 }))
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)
new ContentDisposition('attachment; filename="example.pdf"')
new ContentDisposition({ type: 'attachment', filename: 'example.pdf' })
let headers = new Headers({
'Content-Disposition': new ContentDisposition({ type: 'attachment', filename: 'example.pdf' }),
})
headers.set(
'Content-Disposition',
new ContentDisposition({ type: 'attachment', filename: 'example.pdf' }),
)
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 }),
})
headers.set('Content-Range', new ContentRange({ unit: 'bytes', start: 0, end: 499, size: 1000 }))
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)
new ContentType('text/html; charset=utf-8')
new ContentType({ mediaType: 'text/html', charset: 'utf-8' })
let headers = new Headers({
'Content-Type': new ContentType({ mediaType: 'text/html', charset: 'utf-8' }),
})
headers.set('Content-Type', new ContentType({ mediaType: 'text/html', charset: 'utf-8' }))
Cookie
Parse, manipulate and stringify Cookie headers.
Implements Map<name, value>.
import { Cookie } from 'remix/headers'
let cookie = Cookie.from(request.headers.get('Cookie'))
cookie.get('session_id')
cookie.get('theme')
cookie.has('session_id')
cookie.size
for (let [name, value] of cookie) {
}
cookie.set('theme', 'light')
cookie.delete('session_id')
headers.set('Cookie', cookie)
new Cookie('session_id=abc123; theme=dark')
new Cookie({ session_id: 'abc123', theme: 'dark' })
new Cookie([
['session_id', 'abc123'],
['theme', 'dark'],
])
let headers = new Headers({
Cookie: new Cookie({ session_id: 'abc123', theme: 'dark' }),
})
headers.set('Cookie', new Cookie({ session_id: 'abc123', theme: 'dark' }))
If-Match
Parse, manipulate and stringify If-Match headers.
Implements Set<etag>.
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.add('"newetag"')
ifMatch.delete('"67ab43"')
headers.set('If-Match', ifMatch)
new IfMatch(['abc123', 'def456'])
let headers = new Headers({
'If-Match': new IfMatch(['"abc123"', '"def456"']),
})
headers.set('If-Match', new IfMatch(['"abc123"', '"def456"']))
If-None-Match
Parse, manipulate and stringify If-None-Match headers.
Implements Set<etag>.
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.add('"newetag"')
ifNoneMatch.delete('"67ab43"')
headers.set('If-None-Match', ifNoneMatch)
new IfNoneMatch(['abc123'])
let headers = new Headers({
'If-None-Match': new IfNoneMatch(['"abc123"']),
})
headers.set('If-None-Match', new IfNoneMatch(['"abc123"']))
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 empty = IfRange.from(null)
empty.matches({ etag: '"any"' })
new IfRange('"abc123"')
let headers = new Headers({
'If-Range': new IfRange('"abc123"'),
})
headers.set('If-Range', new IfRange('"abc123"'))
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 }] }),
})
headers.set('Range', new Range({ unit: 'bytes', ranges: [{ start: 0, end: 999 }] }))
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)
new SetCookie('session_id=abc; Path=/; HttpOnly; Secure')
new SetCookie({
name: 'session_id',
value: 'abc',
path: '/',
httpOnly: true,
secure: true,
})
let headers = new Headers({
'Set-Cookie': new SetCookie({ name: 'session_id', value: 'abc', httpOnly: true }),
})
headers.set('Set-Cookie', new SetCookie({ name: 'session_id', value: 'abc', httpOnly: true }))
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)
new Vary('Accept-Encoding, Accept-Language')
new Vary(['Accept-Encoding', 'Accept-Language'])
new Vary({ headerNames: ['Accept-Encoding', 'Accept-Language'] })
let headers = new Headers({
Vary: new Vary(['Accept-Encoding', 'Accept-Language']),
})
headers.set('Vary', new Vary(['Accept-Encoding', 'Accept-Language']))
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