diff options
author | Bartek IwaĆczuk <biwanczuk@gmail.com> | 2020-09-18 15:20:55 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-09-18 09:20:55 -0400 |
commit | 7845740637eb646c0b13dc541f043fd65136fc03 (patch) | |
tree | 8576a376d72ffdfe4ffed983a2bed9e605d20e8b /op_crates/fetch/20_headers.js | |
parent | cead79f5b8ffd376d339b6e0c30e872bfe6820f6 (diff) |
refactor: deno_fetch op crate (#7524)
Diffstat (limited to 'op_crates/fetch/20_headers.js')
-rw-r--r-- | op_crates/fetch/20_headers.js | 256 |
1 files changed, 256 insertions, 0 deletions
diff --git a/op_crates/fetch/20_headers.js b/op_crates/fetch/20_headers.js new file mode 100644 index 000000000..c2ae72864 --- /dev/null +++ b/op_crates/fetch/20_headers.js @@ -0,0 +1,256 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const { DomIterableMixin } = window.__bootstrap.domIterable; + const { requiredArguments } = window.__bootstrap.fetchUtil; + + // From node-fetch + // Copyright (c) 2016 David Frank. MIT License. + const invalidTokenRegex = /[^\^_`a-zA-Z\-0-9!#$%&'*+.|~]/; + const invalidHeaderCharRegex = /[^\t\x20-\x7e\x80-\xff]/; + + function isHeaders(value) { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + return value instanceof Headers; + } + + const headersData = Symbol("headers data"); + + // TODO: headerGuard? Investigate if it is needed + // node-fetch did not implement this but it is in the spec + function normalizeParams(name, value) { + name = String(name).toLowerCase(); + value = String(value).trim(); + return [name, value]; + } + + // The following name/value validations are copied from + // https://github.com/bitinn/node-fetch/blob/master/src/headers.js + // Copyright (c) 2016 David Frank. MIT License. + function validateName(name) { + if (invalidTokenRegex.test(name) || name === "") { + throw new TypeError(`${name} is not a legal HTTP header name`); + } + } + + function validateValue(value) { + if (invalidHeaderCharRegex.test(value)) { + throw new TypeError(`${value} is not a legal HTTP header value`); + } + } + + /** Appends a key and value to the header list. + * + * The spec indicates that when a key already exists, the append adds the new + * value onto the end of the existing value. The behaviour of this though + * varies when the key is `set-cookie`. In this case, if the key of the cookie + * already exists, the value is replaced, but if the key of the cookie does not + * exist, and additional `set-cookie` header is added. + * + * The browser specification of `Headers` is written for clients, and not + * servers, and Deno is a server, meaning that it needs to follow the patterns + * expected for servers, of which a `set-cookie` header is expected for each + * unique cookie key, but duplicate cookie keys should not exist. */ + function dataAppend( + data, + key, + value, + ) { + for (let i = 0; i < data.length; i++) { + const [dataKey] = data[i]; + if (key === "set-cookie" && dataKey === "set-cookie") { + const [, dataValue] = data[i]; + const [dataCookieKey] = dataValue.split("="); + const [cookieKey] = value.split("="); + if (dataCookieKey === cookieKey) { + data[i][1] = value; + return; + } + } else { + if (dataKey === key) { + data[i][1] += `, ${value}`; + return; + } + } + } + data.push([key, value]); + } + + /** Gets a value of a key in the headers list. + * + * This varies slightly from spec behaviour in that when the key is `set-cookie` + * the value returned will look like a concatenated value, when in fact, if the + * headers were iterated over, each individual `set-cookie` value is a unique + * entry in the headers list. */ + function dataGet( + data, + key, + ) { + const setCookieValues = []; + for (const [dataKey, value] of data) { + if (dataKey === key) { + if (key === "set-cookie") { + setCookieValues.push(value); + } else { + return value; + } + } + } + if (setCookieValues.length) { + return setCookieValues.join(", "); + } + return undefined; + } + + /** Sets a value of a key in the headers list. + * + * The spec indicates that the value should be replaced if the key already + * exists. The behaviour here varies, where if the key is `set-cookie` the key + * of the cookie is inspected, and if the key of the cookie already exists, + * then the value is replaced. If the key of the cookie is not found, then + * the value of the `set-cookie` is added to the list of headers. + * + * The browser specification of `Headers` is written for clients, and not + * servers, and Deno is a server, meaning that it needs to follow the patterns + * expected for servers, of which a `set-cookie` header is expected for each + * unique cookie key, but duplicate cookie keys should not exist. */ + function dataSet( + data, + key, + value, + ) { + for (let i = 0; i < data.length; i++) { + const [dataKey] = data[i]; + if (dataKey === key) { + // there could be multiple set-cookie headers, but all others are unique + if (key === "set-cookie") { + const [, dataValue] = data[i]; + const [dataCookieKey] = dataValue.split("="); + const [cookieKey] = value.split("="); + if (cookieKey === dataCookieKey) { + data[i][1] = value; + return; + } + } else { + data[i][1] = value; + return; + } + } + } + data.push([key, value]); + } + + function dataDelete(data, key) { + let i = 0; + while (i < data.length) { + const [dataKey] = data[i]; + if (dataKey === key) { + data.splice(i, 1); + } else { + i++; + } + } + } + + function dataHas(data, key) { + for (const [dataKey] of data) { + if (dataKey === key) { + return true; + } + } + return false; + } + + // ref: https://fetch.spec.whatwg.org/#dom-headers + class HeadersBase { + constructor(init) { + if (init === null) { + throw new TypeError( + "Failed to construct 'Headers'; The provided value was not valid", + ); + } else if (isHeaders(init)) { + this[headersData] = [...init]; + } else { + this[headersData] = []; + if (Array.isArray(init)) { + for (const tuple of init) { + // If header does not contain exactly two items, + // then throw a TypeError. + // ref: https://fetch.spec.whatwg.org/#concept-headers-fill + requiredArguments( + "Headers.constructor tuple array argument", + tuple.length, + 2, + ); + + this.append(tuple[0], tuple[1]); + } + } else if (init) { + for (const [rawName, rawValue] of Object.entries(init)) { + this.append(rawName, rawValue); + } + } + } + } + + [Symbol.for("Deno.customInspect")]() { + let length = this[headersData].length; + let output = ""; + for (const [key, value] of this[headersData]) { + const prefix = length === this[headersData].length ? " " : ""; + const postfix = length === 1 ? " " : ", "; + output = output + `${prefix}${key}: ${value}${postfix}`; + length--; + } + return `Headers {${output}}`; + } + + // ref: https://fetch.spec.whatwg.org/#concept-headers-append + append(name, value) { + requiredArguments("Headers.append", arguments.length, 2); + const [newname, newvalue] = normalizeParams(name, value); + validateName(newname); + validateValue(newvalue); + dataAppend(this[headersData], newname, newvalue); + } + + delete(name) { + requiredArguments("Headers.delete", arguments.length, 1); + const [newname] = normalizeParams(name); + validateName(newname); + dataDelete(this[headersData], newname); + } + + get(name) { + requiredArguments("Headers.get", arguments.length, 1); + const [newname] = normalizeParams(name); + validateName(newname); + return dataGet(this[headersData], newname) ?? null; + } + + has(name) { + requiredArguments("Headers.has", arguments.length, 1); + const [newname] = normalizeParams(name); + validateName(newname); + return dataHas(this[headersData], newname); + } + + set(name, value) { + requiredArguments("Headers.set", arguments.length, 2); + const [newname, newvalue] = normalizeParams(name, value); + validateName(newname); + validateValue(newvalue); + dataSet(this[headersData], newname, newvalue); + } + + get [Symbol.toStringTag]() { + return "Headers"; + } + } + + class Headers extends DomIterableMixin(HeadersBase, headersData) {} + + window.__bootstrap.headers = { + Headers, + }; +})(this); |