UNPKG

@remix-run/headers

Version:

A toolkit for working with HTTP headers in JavaScript

750 lines (749 loc) 27.1 kB
import { Accept } from "./accept.js"; import { AcceptEncoding } from "./accept-encoding.js"; import { AcceptLanguage } from "./accept-language.js"; import { CacheControl } from "./cache-control.js"; import { ContentDisposition } from "./content-disposition.js"; import { ContentRange } from "./content-range.js"; import { ContentType } from "./content-type.js"; import { Cookie } from "./cookie.js"; import { canonicalHeaderName } from "./header-names.js"; import {} from "./header-value.js"; import { IfMatch } from "./if-match.js"; import { IfNoneMatch } from "./if-none-match.js"; import { IfRange } from "./if-range.js"; import { Range } from "./range.js"; import { SetCookie } from "./set-cookie.js"; import { Vary } from "./vary.js"; import { isIterable, quoteEtag } from "./utils.js"; const CRLF = '\r\n'; const AcceptKey = 'accept'; const AcceptEncodingKey = 'accept-encoding'; const AcceptLanguageKey = 'accept-language'; const AcceptRangesKey = 'accept-ranges'; const AgeKey = 'age'; const AllowKey = 'allow'; const CacheControlKey = 'cache-control'; const ConnectionKey = 'connection'; const ContentDispositionKey = 'content-disposition'; const ContentEncodingKey = 'content-encoding'; const ContentLanguageKey = 'content-language'; const ContentLengthKey = 'content-length'; const ContentRangeKey = 'content-range'; const ContentTypeKey = 'content-type'; const CookieKey = 'cookie'; const DateKey = 'date'; const ETagKey = 'etag'; const ExpiresKey = 'expires'; const HostKey = 'host'; const IfMatchKey = 'if-match'; const IfModifiedSinceKey = 'if-modified-since'; const IfNoneMatchKey = 'if-none-match'; const IfRangeKey = 'if-range'; const IfUnmodifiedSinceKey = 'if-unmodified-since'; const LastModifiedKey = 'last-modified'; const LocationKey = 'location'; const RangeKey = 'range'; const RefererKey = 'referer'; const SetCookieKey = 'set-cookie'; const VaryKey = 'vary'; /** * An enhanced JavaScript `Headers` interface with type-safe access. * * [API Reference](https://github.com/remix-run/remix/tree/main/packages/headers) * * [MDN `Headers` Base Class Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers) */ export class SuperHeaders extends Headers { #map; #setCookies = []; /** * @param init A string, iterable, object, or `Headers` instance to initialize with */ constructor(init) { super(); this.#map = new Map(); if (init) { if (typeof init === 'string') { let lines = init.split(CRLF); for (let line of lines) { let match = line.match(/^([^:]+):(.*)/); if (match) { this.append(match[1].trim(), match[2].trim()); } } } else if (isIterable(init)) { for (let [name, value] of init) { this.append(name, value); } } else if (typeof init === 'object') { for (let name of Object.getOwnPropertyNames(init)) { let value = init[name]; let descriptor = Object.getOwnPropertyDescriptor(SuperHeaders.prototype, name); if (descriptor?.set) { descriptor.set.call(this, value); } else { this.set(name, value.toString()); } } } } } /** * Appends a new header value to the existing set of values for a header, * or adds the header if it does not already exist. * * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/append) * * @param name The name of the header to append to * @param value The value to append */ append(name, value) { let key = name.toLowerCase(); if (key === SetCookieKey) { this.#setCookies.push(value); } else { let existingValue = this.#map.get(key); this.#map.set(key, existingValue ? `${existingValue}, ${value}` : value); } } /** * Removes a header. * * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/delete) * * @param name The name of the header to delete */ delete(name) { let key = name.toLowerCase(); if (key === SetCookieKey) { this.#setCookies = []; } else { this.#map.delete(key); } } /** * Returns a string of all the values for a header, or `null` if the header does not exist. * * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/get) * * @param name The name of the header to get * @return The header value, or `null` if not found */ get(name) { let key = name.toLowerCase(); if (key === SetCookieKey) { return this.getSetCookie().join(', '); } else { let value = this.#map.get(key); if (typeof value === 'string') { return value; } else if (value != null) { let str = value.toString(); return str === '' ? null : str; } else { return null; } } } /** * Returns an array of all values associated with the `Set-Cookie` header. This is * useful when building headers for a HTTP response since multiple `Set-Cookie` headers * must be sent on separate lines. * * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/getSetCookie) * * @return An array of `Set-Cookie` header values */ getSetCookie() { return this.#setCookies.map((v) => (typeof v === 'string' ? v : v.toString())); } /** * Returns `true` if the header is present in the list of headers. * * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/has) * * @param name The name of the header to check * @return `true` if the header is present, `false` otherwise */ has(name) { let key = name.toLowerCase(); return key === SetCookieKey ? this.#setCookies.length > 0 : this.get(key) != null; } /** * Sets a new value for the given header. If the header already exists, the new value * will replace the existing value. * * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/set) * * @param name The name of the header to set * @param value The value to set */ set(name, value) { let key = name.toLowerCase(); if (key === SetCookieKey) { this.#setCookies = [value]; } else { this.#map.set(key, value); } } /** * Returns an iterator of all header keys (lowercase). * * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/keys) * * @return An iterator of header keys */ *keys() { for (let [key] of this) yield key; } /** * Returns an iterator of all header values. * * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/values) * * @return An iterator of header values */ *values() { for (let [, value] of this) yield value; } /** * Returns an iterator of all header key/value pairs. * * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/entries) * * @return An iterator of `[key, value]` tuples */ *entries() { for (let [key] of this.#map) { let str = this.get(key); if (str) yield [key, str]; } for (let value of this.getSetCookie()) { yield [SetCookieKey, value]; } } [Symbol.iterator]() { return this.entries(); } /** * Invokes the `callback` for each header key/value pair. * * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/forEach) * * @param callback The function to call for each pair * @param thisArg The value to use as `this` when calling the callback */ forEach(callback, thisArg) { for (let [key, value] of this) { callback.call(thisArg, value, key, this); } } /** * Returns a string representation of the headers suitable for use in a HTTP message. * * @return The headers formatted for HTTP */ toString() { let lines = []; for (let [key, value] of this) { lines.push(`${canonicalHeaderName(key)}: ${value}`); } return lines.join(CRLF); } // Header-specific getters and setters /** * The `Accept` header is used by clients to indicate the media types that are acceptable * in the response. * * [MDN `Accept` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2) */ get accept() { return this.#getHeaderValue(AcceptKey, Accept); } set accept(value) { this.#setHeaderValue(AcceptKey, Accept, value); } /** * The `Accept-Encoding` header contains information about the content encodings that the client * is willing to accept in the response. * * [MDN `Accept-Encoding` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.4) */ get acceptEncoding() { return this.#getHeaderValue(AcceptEncodingKey, AcceptEncoding); } set acceptEncoding(value) { this.#setHeaderValue(AcceptEncodingKey, AcceptEncoding, value); } /** * The `Accept-Language` header contains information about preferred natural language for the * response. * * [MDN `Accept-Language` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5) */ get acceptLanguage() { return this.#getHeaderValue(AcceptLanguageKey, AcceptLanguage); } set acceptLanguage(value) { this.#setHeaderValue(AcceptLanguageKey, AcceptLanguage, value); } /** * The `Accept-Ranges` header indicates the server's acceptance of range requests. * * [MDN `Accept-Ranges` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Ranges) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7233#section-2.3) */ get acceptRanges() { return this.#getStringValue(AcceptRangesKey); } set acceptRanges(value) { this.#setStringValue(AcceptRangesKey, value); } /** * The `Age` header contains the time in seconds an object was in a proxy cache. * * [MDN `Age` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Age) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7234#section-5.1) */ get age() { return this.#getNumberValue(AgeKey); } set age(value) { this.#setNumberValue(AgeKey, value); } /** * The `Allow` header lists the HTTP methods that are supported by the resource. * * [MDN `Allow` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Allow) * * [HTTP/1.1 Specification](https://httpwg.org/specs/rfc9110.html#field.allow) */ get allow() { return this.#getStringValue(AllowKey); } set allow(value) { this.#setStringValue(AllowKey, Array.isArray(value) ? value.join(', ') : value); } /** * The `Cache-Control` header contains directives for caching mechanisms in both requests and responses. * * [MDN `Cache-Control` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7234#section-5.2) */ get cacheControl() { return this.#getHeaderValue(CacheControlKey, CacheControl); } set cacheControl(value) { this.#setHeaderValue(CacheControlKey, CacheControl, value); } /** * The `Connection` header controls whether the network connection stays open after the current * transaction finishes. * * [MDN `Connection` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7230#section-6.1) */ get connection() { return this.#getStringValue(ConnectionKey); } set connection(value) { this.#setStringValue(ConnectionKey, value); } /** * The `Content-Disposition` header is a response-type header that describes how the payload is displayed. * * [MDN `Content-Disposition` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) * * [RFC 6266](https://datatracker.ietf.org/doc/html/rfc6266) */ get contentDisposition() { return this.#getHeaderValue(ContentDispositionKey, ContentDisposition); } set contentDisposition(value) { this.#setHeaderValue(ContentDispositionKey, ContentDisposition, value); } /** * The `Content-Encoding` header specifies the encoding of the resource. * * Note: If multiple encodings have been used, this value may be a comma-separated list. However, most often this * header will only contain a single value. * * [MDN `Content-Encoding` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding) * * [HTTP/1.1 Specification](https://httpwg.org/specs/rfc9110.html#field.content-encoding) */ get contentEncoding() { return this.#getStringValue(ContentEncodingKey); } set contentEncoding(value) { this.#setStringValue(ContentEncodingKey, Array.isArray(value) ? value.join(', ') : value); } /** * The `Content-Language` header describes the natural language(s) of the intended audience for the response content. * * Note: If the response content is intended for multiple audiences, this value may be a comma-separated list. However, * most often this header will only contain a single value. * * [MDN `Content-Language` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Language) * * [HTTP/1.1 Specification](https://httpwg.org/specs/rfc9110.html#field.content-language) */ get contentLanguage() { return this.#getStringValue(ContentLanguageKey); } set contentLanguage(value) { this.#setStringValue(ContentLanguageKey, Array.isArray(value) ? value.join(', ') : value); } /** * The `Content-Length` header indicates the size of the entity-body in bytes. * * [MDN `Content-Length` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2) */ get contentLength() { return this.#getNumberValue(ContentLengthKey); } set contentLength(value) { this.#setNumberValue(ContentLengthKey, value); } /** * The `Content-Range` header indicates where the content of a response body * belongs in relation to a complete resource. * * [MDN `Content-Range` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range) * * [HTTP/1.1 Specification](https://httpwg.org/specs/rfc9110.html#field.content-range) */ get contentRange() { return this.#getHeaderValue(ContentRangeKey, ContentRange); } set contentRange(value) { this.#setHeaderValue(ContentRangeKey, ContentRange, value); } /** * The `Content-Type` header indicates the media type of the resource. * * [MDN `Content-Type` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7231#section-3.1.1.5) */ get contentType() { return this.#getHeaderValue(ContentTypeKey, ContentType); } set contentType(value) { this.#setHeaderValue(ContentTypeKey, ContentType, value); } /** * The `Cookie` request header contains stored HTTP cookies previously sent by the server with * the `Set-Cookie` header. * * [MDN `Cookie` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cookie) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc6265#section-5.4) */ get cookie() { return this.#getHeaderValue(CookieKey, Cookie); } set cookie(value) { this.#setHeaderValue(CookieKey, Cookie, value); } /** * The `Date` header contains the date and time at which the message was sent. * * [MDN `Date` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.1.2) */ get date() { return this.#getDateValue(DateKey); } set date(value) { this.#setDateValue(DateKey, value); } /** * The `ETag` header provides a unique identifier for the current version of the resource. * * [MDN `ETag` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7232#section-2.3) */ get etag() { return this.#getStringValue(ETagKey); } set etag(value) { this.#setStringValue(ETagKey, typeof value === 'string' ? quoteEtag(value) : value); } /** * The `Expires` header contains the date/time after which the response is considered stale. * * [MDN `Expires` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expires) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7234#section-5.3) */ get expires() { return this.#getDateValue(ExpiresKey); } set expires(value) { this.#setDateValue(ExpiresKey, value); } /** * The `Host` header specifies the domain name of the server and (optionally) the TCP port number. * * [MDN `Host` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7230#section-5.4) */ get host() { return this.#getStringValue(HostKey); } set host(value) { this.#setStringValue(HostKey, value); } /** * The `If-Modified-Since` header makes a request conditional on the last modification date of the * requested resource. * * [MDN `If-Modified-Since` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7232#section-3.3) */ get ifModifiedSince() { return this.#getDateValue(IfModifiedSinceKey); } set ifModifiedSince(value) { this.#setDateValue(IfModifiedSinceKey, value); } /** * The `If-Match` header makes a request conditional on the presence of a matching ETag. * * [MDN `If-Match` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Match) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7232#section-3.1) */ get ifMatch() { return this.#getHeaderValue(IfMatchKey, IfMatch); } set ifMatch(value) { this.#setHeaderValue(IfMatchKey, IfMatch, value); } /** * The `If-None-Match` header makes a request conditional on the absence of a matching ETag. * * [MDN `If-None-Match` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7232#section-3.2) */ get ifNoneMatch() { return this.#getHeaderValue(IfNoneMatchKey, IfNoneMatch); } set ifNoneMatch(value) { this.#setHeaderValue(IfNoneMatchKey, IfNoneMatch, value); } /** * The `If-Range` header makes a range request conditional on the resource state. * Can contain either an entity tag (ETag) or an HTTP date. * * [MDN `If-Range` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Range) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7233#section-3.2) */ get ifRange() { return this.#getHeaderValue(IfRangeKey, IfRange); } set ifRange(value) { this.#setHeaderValue(IfRangeKey, IfRange, value); } /** * The `If-Unmodified-Since` header makes a request conditional on the last modification date of the * requested resource. * * [MDN `If-Unmodified-Since` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Unmodified-Since) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7232#section-3.4) */ get ifUnmodifiedSince() { return this.#getDateValue(IfUnmodifiedSinceKey); } set ifUnmodifiedSince(value) { this.#setDateValue(IfUnmodifiedSinceKey, value); } /** * The `Last-Modified` header contains the date and time at which the resource was last modified. * * [MDN `Last-Modified` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7232#section-2.2) */ get lastModified() { return this.#getDateValue(LastModifiedKey); } set lastModified(value) { this.#setDateValue(LastModifiedKey, value); } /** * The `Location` header indicates the URL to redirect to. * * [MDN `Location` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.2) */ get location() { return this.#getStringValue(LocationKey); } set location(value) { this.#setStringValue(LocationKey, value); } /** * The `Range` header indicates the part of a resource that the client wants to receive. * * [MDN `Range` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range) * * [HTTP/1.1 Specification](https://httpwg.org/specs/rfc9110.html#field.range) */ get range() { return this.#getHeaderValue(RangeKey, Range); } set range(value) { this.#setHeaderValue(RangeKey, Range, value); } /** * The `Referer` header contains the address of the previous web page from which a link to the * currently requested page was followed. * * [MDN `Referer` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7231#section-5.5.2) */ get referer() { return this.#getStringValue(RefererKey); } set referer(value) { this.#setStringValue(RefererKey, value); } /** * The `Set-Cookie` header is used to send cookies from the server to the user agent. * * [MDN `Set-Cookie` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc6265#section-4.1) */ get setCookie() { let setCookies = this.#setCookies; for (let i = 0; i < setCookies.length; ++i) { if (typeof setCookies[i] === 'string') { setCookies[i] = new SetCookie(setCookies[i]); } } return setCookies; } set setCookie(value) { if (value != null) { this.#setCookies = (Array.isArray(value) ? value : [value]).map((v) => typeof v === 'string' ? v : new SetCookie(v)); } else { this.#setCookies = []; } } /** * The `Vary` header indicates the set of request headers that determine whether * a cached response can be used rather than requesting a fresh response from the origin server. * * Common values include `Accept-Encoding`, `Accept-Language`, `Accept`, `User-Agent`, etc. * * [MDN `Vary` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary) * * [HTTP/1.1 Specification](https://httpwg.org/specs/rfc9110.html#field.vary) */ get vary() { return this.#getHeaderValue(VaryKey, Vary); } set vary(value) { this.#setHeaderValue(VaryKey, Vary, value); } // Helpers #getHeaderValue(key, ctor) { let value = this.#map.get(key); if (value !== undefined) { if (typeof value === 'string') { let obj = new ctor(value); this.#map.set(key, obj); // cache the new object return obj; } else { return value; } } let obj = new ctor(); this.#map.set(key, obj); // cache the new object return obj; } #setHeaderValue(key, ctor, value) { if (value != null) { this.#map.set(key, typeof value === 'string' ? value : new ctor(value)); } else { this.#map.delete(key); } } #getDateValue(key) { let value = this.#map.get(key); return value === undefined ? null : new Date(value); } #setDateValue(key, value) { if (value != null) { this.#map.set(key, typeof value === 'string' ? value : (typeof value === 'number' ? new Date(value) : value).toUTCString()); } else { this.#map.delete(key); } } #getNumberValue(key) { let value = this.#map.get(key); return value === undefined ? null : parseInt(value, 10); } #setNumberValue(key, value) { if (value != null) { this.#map.set(key, typeof value === 'string' ? value : value.toString()); } else { this.#map.delete(key); } } #getStringValue(key) { let value = this.#map.get(key); return value === undefined ? null : value; } #setStringValue(key, value) { if (value != null) { this.#map.set(key, value); } else { this.#map.delete(key); } } }