Skip to content

headers

The headers package provides typed classes for parsing and serializing common HTTP headers. Each class offers a structured API for reading and writing header values instead of manual string manipulation.

Installation

The headers package is included with Remix. No additional installation is needed.

Import

ts
import {
  Accept,
  AcceptEncoding,
  AcceptLanguage,
  CacheControl,
  ContentDisposition,
  ContentRange,
  ContentType,
  Cookie,
  SetCookie,
  IfMatch,
  IfNoneMatch,
  IfRange,
  Range,
  Vary,
} from 'remix/headers'

Common Pattern

Every header class follows the same pattern:

  • parse(value) -- Static method that parses a header string into a typed object.
  • stringify() -- Serializes the object back to a header string.
  • Constructor -- Creates a new instance from typed values.
ts
let cache = CacheControl.parse('public, max-age=3600')
console.log(cache.maxAge) // 3600
console.log(cache.stringify()) // 'public, max-age=3600'

Content Negotiation

Accept

Parses and works with the Accept request header for content type negotiation.

ts
let accept = Accept.parse(request.headers.get('Accept')!)

// Check if the client accepts JSON
accept.accepts('application/json') // true

// Get all accepted types, ordered by preference
accept.types // [{ type: 'text/html', quality: 1 }, { type: 'application/json', quality: 0.9 }]

// Negotiate the best type from a list
accept.negotiate(['text/html', 'application/json']) // 'text/html'

AcceptEncoding

Parses the Accept-Encoding request header for compression negotiation.

ts
let encoding = AcceptEncoding.parse(request.headers.get('Accept-Encoding')!)

encoding.accepts('br')    // true
encoding.accepts('gzip')  // true

// Get preferred encoding from a list of supported encodings
encoding.negotiate(['br', 'gzip', 'deflate']) // 'br'

AcceptLanguage

Parses the Accept-Language request header for language negotiation.

ts
let lang = AcceptLanguage.parse(request.headers.get('Accept-Language')!)

lang.accepts('en-US') // true

// Negotiate the best language from available translations
lang.negotiate(['en', 'fr', 'de', 'ja']) // 'en'

lang.languages // [{ language: 'en-US', quality: 1 }, { language: 'fr', quality: 0.8 }]

Caching

CacheControl

Parses and builds Cache-Control headers with typed directives.

ts
let cache = CacheControl.parse('public, max-age=3600, s-maxage=86400')

cache.public    // true
cache.maxAge    // 3600
cache.sMaxAge   // 86400
cache.noCache   // false
cache.noStore   // false
cache.stringify() // 'public, max-age=3600, s-maxage=86400'

Create a new Cache-Control header:

ts
let cache = new CacheControl({
  public: true,
  maxAge: 3600,
  staleWhileRevalidate: 60,
})

cache.stringify() // 'public, max-age=3600, stale-while-revalidate=60'
PropertyTypeDescription
publicbooleanResponse can be stored by any cache.
privatebooleanResponse is for a single user only.
noCachebooleanCache must revalidate before use.
noStorebooleanResponse must not be cached.
maxAgenumberTime in seconds the response is fresh.
sMaxAgenumberTime in seconds for shared caches.
mustRevalidatebooleanCache must not use stale responses.
staleWhileRevalidatenumberSeconds a stale response can be used while revalidating.
staleIfErrornumberSeconds a stale response can be used if revalidation fails.
immutablebooleanResponse will not change during its freshness lifetime.

Content Headers

ContentType

Parses and builds Content-Type headers.

ts
let ct = ContentType.parse('text/html; charset=utf-8')

ct.type    // 'text/html'
ct.charset // 'utf-8'
ct.stringify() // 'text/html; charset=utf-8'
ts
let ct = new ContentType('application/json', { charset: 'utf-8' })
ct.stringify() // 'application/json; charset=utf-8'

ContentDisposition

Parses and builds Content-Disposition headers for file downloads and inline display.

ts
let cd = ContentDisposition.parse('attachment; filename="report.pdf"')

cd.type     // 'attachment'
cd.filename // 'report.pdf'
cd.stringify() // 'attachment; filename="report.pdf"'
ts
let cd = new ContentDisposition('attachment', { filename: 'data.csv' })
cd.stringify() // 'attachment; filename="data.csv"'

ContentRange

Parses and builds Content-Range response headers for byte-range responses.

ts
let cr = ContentRange.parse('bytes 0-1023/4096')

cr.unit  // 'bytes'
cr.start // 0
cr.end   // 1023
cr.size  // 4096
cr.stringify() // 'bytes 0-1023/4096'

Cookies

Parses the Cookie request header into a key/value map.

ts
let cookies = Cookie.parse(request.headers.get('Cookie')!)

cookies.get('session')  // 'abc123'
cookies.get('theme')    // 'dark'
cookies.has('session')  // true

SetCookie

Builds Set-Cookie response headers with all standard attributes.

ts
let cookie = new SetCookie('session', 'abc123', {
  httpOnly: true,
  secure: true,
  sameSite: 'lax',
  maxAge: 86400,
  path: '/',
})

cookie.stringify()
// 'session=abc123; HttpOnly; Secure; SameSite=Lax; Max-Age=86400; Path=/'

Parse an existing Set-Cookie header:

ts
let cookie = SetCookie.parse('session=abc123; HttpOnly; Secure; Path=/')

cookie.name     // 'session'
cookie.value    // 'abc123'
cookie.httpOnly // true
cookie.secure   // true
cookie.path     // '/'

Conditional Requests

IfMatch

Parses the If-Match request header for conditional requests based on ETags.

ts
let im = IfMatch.parse(request.headers.get('If-Match')!)

im.matches('"abc123"') // true
im.any                  // false (true when header is '*')
im.etags                // ['"abc123"', '"def456"']

IfNoneMatch

Parses the If-None-Match request header.

ts
let inm = IfNoneMatch.parse(request.headers.get('If-None-Match')!)

inm.matches('"abc123"') // true
inm.any                  // false

IfRange

Parses the If-Range request header, which can be either an ETag or a date.

ts
let ir = IfRange.parse(request.headers.get('If-Range')!)

ir.etag // '"abc123"' or undefined
ir.date // Date object or undefined

Range

Parses the Range request header for byte-range requests.

ts
let range = Range.parse(request.headers.get('Range')!)

range.unit   // 'bytes'
range.ranges // [{ start: 0, end: 1023 }, { start: 2048, end: 4095 }]

Other Headers

Vary

Parses and builds the Vary response header.

ts
let vary = Vary.parse(response.headers.get('Vary')!)

vary.headers // ['Accept', 'Accept-Encoding']
vary.has('Accept') // true

vary.add('Accept-Language')
vary.stringify() // 'Accept, Accept-Encoding, Accept-Language'

Examples

Content Negotiation

ts
import { Accept, AcceptEncoding } from 'remix/headers'

router.map(apiRoute, async ({ request }) => {
  let accept = Accept.parse(request.headers.get('Accept') ?? '*/*')

  if (accept.accepts('application/json')) {
    return Response.json({ message: 'Hello' })
  }

  return new Response('Hello', {
    headers: { 'Content-Type': 'text/plain' },
  })
})

Cache Headers

ts
import { CacheControl } from 'remix/headers'

let cache = new CacheControl({
  public: true,
  maxAge: 60,
  staleWhileRevalidate: 300,
})

return new Response(body, {
  headers: {
    'Cache-Control': cache.stringify(),
  },
})

File Download

ts
import { ContentDisposition } from 'remix/headers'

let cd = new ContentDisposition('attachment', {
  filename: 'report-2024.pdf',
})

return new Response(fileStream, {
  headers: {
    'Content-Disposition': cd.stringify(),
    'Content-Type': 'application/pdf',
  },
})
ts
import { SetCookie } from 'remix/headers'

let cookie = new SetCookie('theme', 'dark', {
  maxAge: 365 * 24 * 60 * 60,
  path: '/',
  sameSite: 'lax',
})

return new Response('OK', {
  headers: {
    'Set-Cookie': cookie.stringify(),
  },
})
  • cookie --- Higher-level cookie management.
  • response --- Response helpers that set common headers automatically.
  • Middleware --- Use headers in middleware for cross-cutting concerns.

Released under the MIT License.