UNPKG

@remix-run/headers

Version:

A toolkit for working with HTTP headers in JavaScript

591 lines (434 loc) 16.5 kB
# headers Tired of manually parsing and stringifying HTTP header values in JavaScript? `headers` supercharges the standard `Headers` interface, providing a robust toolkit for effortless and type-safe header manipulation. HTTP headers are packed with critical information—from content negotiation and caching directives to authentication tokens and file metadata. While the native `Headers` API provides a basic string-based interface, it leaves the complexities of parsing specific header formats (like `Accept`, `Content-Type`, or `Set-Cookie`) entirely up to you. ## Features - **Type-Safe Accessors:** Interact with complex header values (e.g., media types, quality factors, cookie attributes) through strongly-typed properties and methods, eliminating guesswork and manual parsing. - **Automatic Parsing & Stringification:** The library intelligently handles the parsing of raw header strings into structured objects and stringifies your structured data back into spec-compliant header values. - **Fluent Interface:** Enjoy a more expressive and developer-friendly API for reading and writing header information. - **Drop-in Enhancement:** As a subclass of the standard `Headers` object, it can be used anywhere a `Headers` object is expected, providing progressive enhancement to your existing code. - **Individual Header Utilities:** For fine-grained control, use standalone utility classes for specific headers, perfect for scenarios outside of a full `Headers` object. Unlock a more powerful and elegant way to work with HTTP headers in your JavaScript and TypeScript projects! ## Installation ```sh npm install @remix-run/headers ``` ## Overview The following should give you a sense of what kinds of things you can do with this library: ```ts import Headers from '@remix-run/headers' let headers = new Headers() // Accept headers.accept = 'text/html, text/*;q=0.9' headers.accept.mediaTypes // [ 'text/html', 'text/*' ] Object.fromEntries(headers.accept.entries()) // { 'text/html': 1, 'text/*': 0.9 } headers.accept.accepts('text/html') // true headers.accept.accepts('text/plain') // true headers.accept.accepts('image/jpeg') // false headers.accept.getPreferred(['text/plain', 'text/html']) // 'text/html' headers.accept.set('text/plain', 0.9) headers.accept.set('text/*', 0.8) headers.get('Accept') // 'text/html,text/plain;q=0.9,text/*;q=0.8' // Accept-Encoding headers.acceptEncoding = 'gzip, deflate;q=0.8' headers.acceptEncoding.encodings // [ 'gzip', 'deflate' ] Object.fromEntries(headers.acceptEncoding.entries()) // { 'gzip': 1, 'deflate': 0.8 } headers.acceptEncoding.accepts('gzip') // true headers.acceptEncoding.accepts('br') // false headers.acceptEncoding.getPreferred(['gzip', 'deflate']) // 'gzip' // Accept-Language headers.acceptLanguage = 'en-US, en;q=0.9' headers.acceptLanguage.languages // [ 'en-us', 'en' ] Object.fromEntries(headers.acceptLanguage.entries()) // { 'en-us': 1, en: 0.9 } headers.acceptLanguage.accepts('en') // true headers.acceptLanguage.accepts('ja') // false headers.acceptLanguage.getPreferred(['en-US', 'en-GB']) // 'en-US' headers.acceptLanguage.getPreferred(['en', 'fr']) // 'en' // Accept-Ranges headers.acceptRanges = 'bytes' // Allow headers.allow = ['GET', 'POST', 'PUT'] headers.get('Allow') // 'GET, POST, PUT' // Connection headers.connection = 'close' // Content-Type headers.contentType = 'application/json; charset=utf-8' headers.contentType.mediaType // "application/json" headers.contentType.charset // "utf-8" headers.contentType.charset = 'iso-8859-1' headers.get('Content-Type') // "application/json; charset=iso-8859-1" // Content-Disposition headers.contentDisposition = 'attachment; filename="example.pdf"; filename*=UTF-8\'\'%E4%BE%8B%E5%AD%90.pdf' headers.contentDisposition.type // 'attachment' headers.contentDisposition.filename // 'example.pdf' headers.contentDisposition.filenameSplat // 'UTF-8\'\'%E4%BE%8B%E5%AD%90.pdf' headers.contentDisposition.preferredFilename // '例子.pdf' // Cookie headers.cookie = 'session_id=abc123; user_id=12345' headers.cookie.get('session_id') // 'abc123' headers.cookie.get('user_id') // '12345' headers.cookie.set('theme', 'dark') headers.get('Cookie') // 'session_id=abc123; user_id=12345; theme=dark' // Host headers.host = 'example.com' // If-Match headers.ifMatch = ['67ab43', '54ed21'] headers.get('If-Match') // '"67ab43", "54ed21"' headers.ifMatch.matches('67ab43') // true headers.ifMatch.matches('abc123') // false // If-None-Match headers.ifNoneMatch = ['67ab43', '54ed21'] headers.get('If-None-Match') // '"67ab43", "54ed21"' headers.ifNoneMatch.matches('67ab43') // true headers.ifNoneMatch.matches('abc123') // false // If-Range headers.ifRange = new Date('2021-01-01T00:00:00Z') headers.get('If-Range') // 'Fri, 01 Jan 2021 00:00:00 GMT' headers.ifRange.matches({ lastModified: 1609459200000 }) // true (timestamp) headers.ifRange.matches({ lastModified: new Date('2021-01-01T00:00:00Z') }) // true (Date) // Last-Modified headers.lastModified = new Date('2021-01-01T00:00:00Z') // or headers.lastModified = new Date('2021-01-01T00:00:00Z').getTime(); headers.get('Last-Modified') // 'Fri, 01 Jan 2021 00:00:00 GMT' // Location headers.location = 'https://example.com' // Range headers.range = 'bytes=200-1000' headers.range.unit // "bytes" headers.range.ranges // [{ start: 200, end: 1000 }] headers.range.canSatisfy(2000) // true // Referer headers.referer = 'https://example.com/' // Set-Cookie headers.setCookie = ['session_id=abc123; Path=/; HttpOnly'] headers.setCookie[0].name // 'session_id' headers.setCookie[0].value // 'abc123' headers.setCookie[0].path // '/' headers.setCookie[0].httpOnly // true // Modifying Set-Cookie attributes headers.setCookie[0].maxAge = 3600 headers.setCookie[0].secure = true headers.get('Set-Cookie') // 'session_id=abc123; Path=/; HttpOnly; Max-Age=3600; Secure' // Setting multiple cookies is easy, it's just an array headers.setCookie.push('user_id=12345; Path=/api; Secure') // or headers.setCookie = [...headers.setCookie, '...'] // Accessing multiple Set-Cookie headers for (let cookie of headers.getSetCookie()) { console.log(cookie) } // session_id=abc123; Path=/; HttpOnly; Max-Age=3600; Secure // user_id=12345; Path=/api; Secure ``` `Headers` can be initialized with an object config: ```ts let headers = new Headers({ contentType: { mediaType: 'text/html', charset: 'utf-8', }, setCookie: [ { name: 'session', value: 'abc', path: '/' }, { name: 'theme', value: 'dark', expires: new Date('2021-12-31T23:59:59Z') }, ], }) console.log(`${headers}`) // Content-Type: text/html; charset=utf-8 // Set-Cookie: session=abc; Path=/ // Set-Cookie: theme=dark; Expires=Fri, 31 Dec 2021 23:59:59 GMT ``` `Headers` works just like [DOM's `Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers) (it's a subclass) so you can use them anywhere you need a `Headers`. ```ts import Headers from '@remix-run/headers' // Use in a fetch() let response = await fetch('https://example.com', { headers: new Headers(), }) // Convert from DOM Headers let headers = new Headers(response.headers) headers.set('Content-Type', 'text/html') headers.get('Content-Type') // "text/html" ``` If you're familiar with using DOM `Headers`, everything works as you'd expect. `Headers` are iterable: ```ts let headers = new Headers({ 'Content-Type': 'application/json', 'X-API-Key': 'secret-key', 'Accept-Language': 'en-US,en;q=0.9', }) for (let [name, value] of headers) { console.log(`${name}: ${value}`) } // Content-Type: application/json // X-Api-Key: secret-key // Accept-Language: en-US,en;q=0.9 ``` If you're assembling HTTP messages, you can easily convert to a multiline string suitable for using as a Request/Response header block: ```ts let headers = new Headers({ 'Content-Type': 'application/json', 'Accept-Language': 'en-US,en;q=0.9', }) console.log(`${headers}`) // Content-Type: application/json // Accept-Language: en-US,en;q=0.9 ``` ## Individual Header Utility Classes In addition to the high-level `Headers` API, `headers` also provides a rich set of primitives you can use to work with just about any complex HTTP header value. Each header class includes a spec-compliant parser (the constructor), stringifier (`toString`), and getters/setters for all relevant attributes. Classes for headers that contain a list of fields, like `Cookie`, are iterable. If you need support for a header that isn't listed here, please [send a PR](https://github.com/remix-run/remix/pulls)! The goal is to have first-class support for all common HTTP headers. ### Accept ```ts import { Accept } from '@remix-run/headers' let header = new Accept('text/html;text/*;q=0.9') header.has('text/html') // true header.has('text/plain') // false header.accepts('text/html') // true header.accepts('text/plain') // true header.accepts('text/*') // true header.accepts('image/jpeg') // false header.getPreferred(['text/html', 'text/plain']) // 'text/html' for (let [mediaType, quality] of header) { // ... } // Alternative init styles let header = new Accept({ 'text/html': 1, 'text/*': 0.9 }) let header = new Accept(['text/html', ['text/*', 0.9]]) ``` ### Accept-Encoding ```ts import { AcceptEncoding } from '@remix-run/headers' let header = new AcceptEncoding('gzip,deflate;q=0.9') header.has('gzip') // true header.has('br') // false header.accepts('gzip') // true header.accepts('deflate') // true header.accepts('identity') // true header.accepts('br') // true header.getPreferred(['gzip', 'deflate']) // 'gzip' for (let [encoding, weight] of header) { // ... } // Alternative init styles let header = new AcceptEncoding({ gzip: 1, deflate: 0.9 }) let header = new AcceptEncoding(['gzip', ['deflate', 0.9]]) ``` ### Accept-Language ```ts import { AcceptLanguage } from '@remix-run/headers' let header = new AcceptLanguage('en-US,en;q=0.9') header.has('en-US') // true header.has('en-GB') // false header.accepts('en-US') // true header.accepts('en-GB') // true header.accepts('en') // true header.accepts('fr') // true header.getPreferred(['en-US', 'en-GB']) // 'en-US' header.getPreferred(['en', 'fr']) // 'en' for (let [language, quality] of header) { // ... } // Alternative init styles let header = new AcceptLanguage({ 'en-US': 1, en: 0.9 }) let header = new AcceptLanguage(['en-US', ['en', 0.9]]) ``` ### Cache-Control ```ts import { CacheControl } from '@remix-run/headers' let header = new CacheControl('public, max-age=3600, s-maxage=3600') header.public // true header.maxAge // 3600 header.sMaxage // 3600 // Alternative init style let header = new CacheControl({ public: true, maxAge: 3600 }) // Full set of supported properties header.public // true/false header.private // true/false header.noCache // true/false header.noStore // true/false header.noTransform // true/false header.mustRevalidate // true/false header.proxyRevalidate // true/false header.maxAge // number header.sMaxage // number header.minFresh // number header.maxStale // number header.onlyIfCached // true/false header.immutable // true/false header.staleWhileRevalidate // number header.staleIfError // number ``` ### Content-Disposition ```ts import { ContentDisposition } from '@remix-run/headers' let header = new ContentDisposition('attachment; name=file1; filename=file1.txt') header.type // "attachment" header.name // "file1" header.filename // "file1.txt" header.preferredFilename // "file1.txt" // Alternative init style let header = new ContentDisposition({ type: 'attachment', name: 'file1', filename: 'file1.txt', }) ``` ### Content-Type ```ts import { ContentType } from '@remix-run/headers' let header = new ContentType('text/html; charset=utf-8') header.mediaType // "text/html" header.boundary // undefined header.charset // "utf-8" // Alternative init style let header = new ContentType({ mediaType: 'multipart/form-data', boundary: '------WebKitFormBoundary12345', charset: 'utf-8', }) ``` ### Content-Range ```ts import { ContentRange } from '@remix-run/headers' // Satisfied range let header = new ContentRange('bytes 200-1000/67589') header.unit // "bytes" header.start // 200 header.end // 1000 header.size // 67589 // Unsatisfied range let header = new ContentRange('bytes */67589') header.unit // "bytes" header.start // null header.end // null header.size // 67589 // Alternative init style let header = new ContentRange({ unit: 'bytes', start: 200, end: 1000, size: 67589, }) ``` ### Cookie ```ts import { Cookie } from '@remix-run/headers' let header = new Cookie('theme=dark; session_id=123') header.get('theme') // "dark" header.set('theme', 'light') header.delete('theme') header.has('session_id') // true // Iterate over cookie name/value pairs for (let [name, value] of header) { // ... } // Alternative init styles let header = new Cookie({ theme: 'dark', session_id: '123' }) let header = new Cookie([ ['theme', 'dark'], ['session_id', '123'], ]) ``` ### If-Match ```ts import { IfMatch } from '@remix-run/headers' let header = new IfMatch('"67ab43", "54ed21"') header.has('67ab43') // true header.has('21ba69') // false // Check if precondition passes header.matches('"67ab43"') // true header.matches('"abc123"') // false // Note: Uses strong comparison only (weak ETags never match) let weakHeader = new IfMatch('W/"67ab43"') weakHeader.matches('W/"67ab43"') // false // Alternative init styles let header = new IfMatch(['67ab43', '54ed21']) let header = new IfMatch({ tags: ['67ab43', '54ed21'], }) ``` ### If-None-Match ```ts import { IfNoneMatch } from '@remix-run/headers' let header = new IfNoneMatch('"67ab43", "54ed21"') header.has('67ab43') // true header.has('21ba69') // false header.matches('"67ab43"') // true // Alternative init styles let header = new IfNoneMatch(['67ab43', '54ed21']) let header = new IfNoneMatch({ tags: ['67ab43', '54ed21'], }) ``` ### If-Range ```ts import { IfRange } from '@remix-run/headers' // Initialize with HTTP date let header = new IfRange('Fri, 01 Jan 2021 00:00:00 GMT') header.matches({ lastModified: 1609459200000 }) // true header.matches({ lastModified: new Date('2021-01-01T00:00:00Z') }) // true (Date also supported) // Initialize with Date object let header = new IfRange(new Date('2021-01-01T00:00:00Z')) header.matches({ lastModified: 1609459200000 }) // true // Initialize with strong ETag let header = new IfRange('"67ab43"') header.matches({ etag: '"67ab43"' }) // true // Never matches weak ETags let weakHeader = new IfRange('W/"67ab43"') header.matches({ etag: 'W/"67ab43"' }) // false // Returns true if header is not present (range should proceed unconditionally) let emptyHeader = new IfRange('') emptyHeader.matches({ etag: '"67ab43"' }) // true ``` ### Range ```ts import { Range } from '@remix-run/headers' let header = new Range('bytes=200-1000') header.unit // "bytes" header.ranges // [{ start: 200, end: 1000 }] // Check if ranges can be satisfied for a given file size header.canSatisfy(2000) // true header.canSatisfy(500) // false (end is beyond file size) // Multiple ranges let header = new Range('bytes=0-499, 1000-1499') header.ranges.length // 2 // Normalize to concrete start/end values for a given file size let header = new Range('bytes=1000-') header.normalize(2000) // [{ start: 1000, end: 1999 }] // Alternative init style let header = new Range({ unit: 'bytes', ranges: [ { start: 200, end: 1000 }, { start: 2000, end: 2999 }, ], }) ``` ### Set-Cookie ```ts import { SetCookie } from '@remix-run/headers' let header = new SetCookie('session_id=abc; Domain=example.com; Path=/; Secure; HttpOnly') header.name // "session_id" header.value // "abc" header.domain // "example.com" header.path // "/" header.secure // true header.httpOnly // true header.sameSite // undefined header.maxAge // undefined header.expires // undefined // Alternative init styles let header = new SetCookie({ name: 'session_id', value: 'abc', domain: 'example.com', path: '/', secure: true, httpOnly: true, }) ``` ## Related Packages - [`fetch-proxy`](https://github.com/remix-run/remix/tree/main/packages/fetch-proxy) - Build HTTP proxy servers using the web fetch API - [`node-fetch-server`](https://github.com/remix-run/remix/tree/main/packages/node-fetch-server) - Build HTTP servers on Node.js using the web fetch API ## License See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)