Skip to main content
Typed utilities for parsing, manipulating, and serializing HTTP header values. The headers package provides focused classes for common HTTP headers with round-trip safety and typed operations.

Installation

npm i remix

Features

  • 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
  • Web Standards - Runtime-agnostic, works in Node.js, Bun, Deno, and edge runtimes

Usage

Each supported header has a class that represents the header value. Use the static from() method to parse header values:
import { Accept, CacheControl } from 'remix/headers'

let accept = Accept.from(request.headers.get('accept'))
accept.accepts('application/json') // true

let cacheControl = CacheControl.from(response.headers.get('cache-control'))
cacheControl.maxAge // 3600

Supported Headers

Accept

Parse and manipulate Accept headers. Implements Map<mediaType, quality>.
import { Accept } from 'remix/headers'

let accept = Accept.from(request.headers.get('accept'))

accept.mediaTypes // ['text/html', 'text/*']
accept.weights // [1, 0.9]
accept.accepts('text/html') // true
accept.accepts('text/plain') // true (matches text/*)
accept.getWeight('text/plain') // 1
accept.getPreferred(['text/html', 'text/plain']) // 'text/html'

Methods

accepts
(mediaType: string) => boolean
Returns true if the header matches the given media type (supports wildcards).
getWeight
(mediaType: string) => number
Gets the weight (quality value) of a media type. Supports wildcards.
getPreferred
<T extends string>(mediaTypes: readonly T[]) => T | null
Returns the most preferred media type from the given list, or null if none match.
get
(mediaType: string) => number | null
Returns the weight if the media type exists verbatim, or null.
set
(mediaType: string, weight?: number) => void
Sets a media type with the given weight (default: 1).
delete
(mediaType: string) => void
Removes the given media type from the header.
has
(mediaType: string) => boolean
Checks if a media type exists in the header (verbatim match).

Properties

mediaTypes
string[]
Array of all media types in the header.
weights
number[]
Array of all quality values in the header.
size
number
The number of media types in the Accept header.

Accept-Encoding

Parse and manipulate Accept-Encoding headers. Implements Map<encoding, quality>.
import { AcceptEncoding } from 'remix/headers'

let acceptEncoding = AcceptEncoding.from(
  request.headers.get('accept-encoding')
)

acceptEncoding.encodings // ['gzip', 'deflate']
acceptEncoding.accepts('gzip') // true
acceptEncoding.getPreferred(['gzip', 'deflate', 'br']) // 'gzip'

// Construct
new AcceptEncoding('gzip, deflate;q=0.8')
new AcceptEncoding({ gzip: 1, deflate: 0.8 })

Accept-Language

Parse and manipulate Accept-Language headers. Implements Map<language, quality>.
import { AcceptLanguage } from 'remix/headers'

let acceptLanguage = AcceptLanguage.from(
  request.headers.get('accept-language')
)

acceptLanguage.languages // ['en-us', 'en']
acceptLanguage.accepts('en-US') // true
acceptLanguage.accepts('en-GB') // true (matches en)
acceptLanguage.getPreferred(['en-US', 'en-GB', 'fr']) // 'en-US'

// Construct
new AcceptLanguage('en-US, en;q=0.9')
new AcceptLanguage({ 'en-US': 1, en: 0.9 })

Cache-Control

Parse and manipulate Cache-Control headers.
import { CacheControl } from 'remix/headers'

let cacheControl = CacheControl.from(
  response.headers.get('cache-control')
)

cacheControl.public // true
cacheControl.maxAge // 3600
cacheControl.sMaxage // 7200
cacheControl.immutable // undefined

// Modify
cacheControl.maxAge = 7200
cacheControl.immutable = true
headers.set('Cache-Control', cacheControl.toString())

// Construct
new CacheControl('public, max-age=3600')
new CacheControl({ public: true, maxAge: 3600 })

Properties

public
boolean | undefined
Indicates the response may be stored by any cache.
private
boolean | undefined
Indicates the response is intended for a single user.
maxAge
number | undefined
Maximum time in seconds a resource is considered fresh.
sMaxage
number | undefined
Shared cache maximum age (overrides max-age for CDNs).
noCache
boolean | undefined
Response must be revalidated with the origin server.
noStore
boolean | undefined
Response must not be stored in any cache.
immutable
boolean | undefined
Indicates the response will never change.
mustRevalidate
boolean | undefined
Stale cache entries must be validated before use.

Content-Disposition

Parse and manipulate Content-Disposition headers.
import { ContentDisposition } from 'remix/headers'

let contentDisposition = ContentDisposition.from(
  response.headers.get('content-disposition')
)

contentDisposition.type // 'attachment'
contentDisposition.filename // 'example.pdf'
contentDisposition.preferredFilename // Decoded filename

// Construct
new ContentDisposition('attachment; filename="example.pdf"')
new ContentDisposition({ type: 'attachment', filename: 'example.pdf' })

Content-Range

Parse and manipulate Content-Range headers.
import { ContentRange } from 'remix/headers'

let contentRange = ContentRange.from(
  response.headers.get('content-range')
)

contentRange.unit // "bytes"
contentRange.start // 200
contentRange.end // 1000
contentRange.size // 67589

// Unsatisfied range
let unsatisfied = ContentRange.from('bytes */67589')
unsatisfied.start // null
unsatisfied.end // null

// Construct
new ContentRange({ unit: 'bytes', start: 0, end: 499, size: 1000 })

Content-Type

Parse and manipulate Content-Type headers.
import { ContentType } from 'remix/headers'

let contentType = ContentType.from(request.headers.get('content-type'))

contentType.mediaType // "text/html"
contentType.charset // "utf-8"
contentType.boundary // undefined (or boundary for multipart)

// Modify
contentType.charset = 'iso-8859-1'
headers.set('Content-Type', contentType.toString())

// Construct
new ContentType('text/html; charset=utf-8')
new ContentType({ mediaType: 'text/html', charset: 'utf-8' })

Parse and manipulate Cookie headers. Implements Map<name, value>.
import { Cookie } from 'remix/headers'

let cookie = Cookie.from(request.headers.get('cookie'))

cookie.get('session_id') // 'abc123'
cookie.get('theme') // 'dark'
cookie.has('session_id') // true
cookie.size // 2

// Modify
cookie.set('theme', 'light')
cookie.delete('session_id')

// Construct
new Cookie('session_id=abc123; theme=dark')
new Cookie({ session_id: 'abc123', theme: 'dark' })
new Cookie([['session_id', 'abc123'], ['theme', 'dark']])

If-Match

Parse and manipulate If-Match headers. Implements Set<etag>.
import { IfMatch } from 'remix/headers'

let ifMatch = IfMatch.from(request.headers.get('if-match'))

ifMatch.tags // ['"67ab43"', '"54ed21"']
ifMatch.has('"67ab43"') // true
ifMatch.matches('"67ab43"') // true (checks precondition)

// Note: Uses strong comparison only
let weak = IfMatch.from('W/"67ab43"')
weak.matches('W/"67ab43"') // false

If-None-Match

Parse and manipulate If-None-Match headers. Implements Set<etag>.
import { IfNoneMatch } from 'remix/headers'

let ifNoneMatch = IfNoneMatch.from(request.headers.get('if-none-match'))

ifNoneMatch.tags // ['"67ab43"', '"54ed21"']
ifNoneMatch.matches('"67ab43"') // true

// Supports weak comparison
let weak = IfNoneMatch.from('W/"67ab43"')
weak.matches('W/"67ab43"') // true

If-Range

Parse and manipulate If-Range headers.
import { IfRange } from 'remix/headers'

let ifRange = IfRange.from(request.headers.get('if-range'))

// With HTTP date
ifRange.matches({ lastModified: 1609459200000 }) // true
ifRange.matches({ lastModified: new Date('2021-01-01') }) // true

// With ETag
let etagHeader = IfRange.from('"67ab43"')
etagHeader.matches({ etag: '"67ab43"' }) // true

// Empty/null (range proceeds unconditionally)
let empty = IfRange.from(null)
empty.matches({ etag: '"any"' }) // true

Range

Parse and manipulate Range headers.
import { Range } from 'remix/headers'

let range = Range.from(request.headers.get('range'))

range.unit // "bytes"
range.ranges // [{ start: 200, end: 1000 }]
range.canSatisfy(2000) // true
range.normalize(2000) // [{ start: 200, end: 1000 }]

// Multiple ranges
let multi = Range.from('bytes=0-499, 1000-1499')
multi.ranges.length // 2

// Suffix range (last N bytes)
let suffix = Range.from('bytes=-500')
suffix.normalize(2000) // [{ start: 1500, end: 1999 }]

// Construct
new Range({ unit: 'bytes', ranges: [{ start: 0, end: 999 }] })

Parse and manipulate Set-Cookie headers.
import { SetCookie } from 'remix/headers'

let setCookie = SetCookie.from(response.headers.get('set-cookie'))

setCookie.name // "session_id"
setCookie.value // "abc"
setCookie.path // "/"
setCookie.httpOnly // true
setCookie.secure // true
setCookie.sameSite // undefined

// Modify
setCookie.maxAge = 3600
setCookie.sameSite = 'Strict'
headers.set('Set-Cookie', setCookie.toString())

// Construct
new SetCookie('session_id=abc; Path=/; HttpOnly; Secure')
new SetCookie({
  name: 'session_id',
  value: 'abc',
  path: '/',
  httpOnly: true,
  secure: true,
})

Vary

Parse and manipulate Vary headers. Implements Set<headerName>.
import { Vary } from 'remix/headers'

let vary = Vary.from(response.headers.get('vary'))

vary.headerNames // ['accept-encoding', 'accept-language']
vary.has('Accept-Encoding') // true (case-insensitive)
vary.size // 2

// Modify
vary.add('User-Agent')
vary.delete('Accept-Language')

// Construct
new Vary('Accept-Encoding, Accept-Language')
new Vary(['Accept-Encoding', 'Accept-Language'])
new Vary({ headerNames: ['Accept-Encoding', 'Accept-Language'] })

Raw Headers

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') // 'text/html'
headers.get('cache-control') // 'no-cache'

stringify(headers)
// 'Content-Type: text/html\r\nCache-Control: no-cache'

Type Definitions

type AcceptInit = Iterable<string | [string, number]> | Record<string, number>
type AcceptEncodingInit = Iterable<string | [string, number]> | Record<string, number>
type AcceptLanguageInit = Iterable<string | [string, number]> | Record<string, number>
type CookieInit = string | Record<string, string> | Iterable<[string, string]>

interface CacheControlInit {
  public?: boolean
  private?: boolean
  maxAge?: number
  sMaxage?: number
  noCache?: boolean
  noStore?: boolean
  noTransform?: boolean
  mustRevalidate?: boolean
  immutable?: boolean
}

interface ContentDispositionInit {
  type: string
  filename?: string
  filenameSplat?: string
}

interface ContentTypeInit {
  mediaType: string
  charset?: string
  boundary?: string
}

interface RangeInit {
  unit: string
  ranges: Array<{ start: number; end: number | null }>
}
  • cookie - Cookie parsing and serialization with signing support
  • response - Response helpers that use header utilities

Build docs developers (and LLMs) love