headers.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. // https://github.com/Ethan-Arrowood/undici-fetch
  2. 'use strict'
  3. const { kHeadersList } = require('../core/symbols')
  4. const { kGuard } = require('./symbols')
  5. const { kEnumerableProperty } = require('../core/util')
  6. const {
  7. makeIterator,
  8. isValidHeaderName,
  9. isValidHeaderValue
  10. } = require('./util')
  11. const { webidl } = require('./webidl')
  12. const assert = require('assert')
  13. const kHeadersMap = Symbol('headers map')
  14. const kHeadersSortedMap = Symbol('headers map sorted')
  15. /**
  16. * @see https://fetch.spec.whatwg.org/#concept-header-value-normalize
  17. * @param {string} potentialValue
  18. */
  19. function headerValueNormalize (potentialValue) {
  20. // To normalize a byte sequence potentialValue, remove
  21. // any leading and trailing HTTP whitespace bytes from
  22. // potentialValue.
  23. // Trimming the end with `.replace()` and a RegExp is typically subject to
  24. // ReDoS. This is safer and faster.
  25. let i = potentialValue.length
  26. while (/[\r\n\t ]/.test(potentialValue.charAt(--i)));
  27. return potentialValue.slice(0, i + 1).replace(/^[\r\n\t ]+/, '')
  28. }
  29. function fill (headers, object) {
  30. // To fill a Headers object headers with a given object object, run these steps:
  31. // 1. If object is a sequence, then for each header in object:
  32. // Note: webidl conversion to array has already been done.
  33. if (Array.isArray(object)) {
  34. for (const header of object) {
  35. // 1. If header does not contain exactly two items, then throw a TypeError.
  36. if (header.length !== 2) {
  37. throw webidl.errors.exception({
  38. header: 'Headers constructor',
  39. message: `expected name/value pair to be length 2, found ${header.length}.`
  40. })
  41. }
  42. // 2. Append (header’s first item, header’s second item) to headers.
  43. headers.append(header[0], header[1])
  44. }
  45. } else if (typeof object === 'object' && object !== null) {
  46. // Note: null should throw
  47. // 2. Otherwise, object is a record, then for each key → value in object,
  48. // append (key, value) to headers
  49. for (const [key, value] of Object.entries(object)) {
  50. headers.append(key, value)
  51. }
  52. } else {
  53. throw webidl.errors.conversionFailed({
  54. prefix: 'Headers constructor',
  55. argument: 'Argument 1',
  56. types: ['sequence<sequence<ByteString>>', 'record<ByteString, ByteString>']
  57. })
  58. }
  59. }
  60. class HeadersList {
  61. /** @type {[string, string][]|null} */
  62. cookies = null
  63. constructor (init) {
  64. if (init instanceof HeadersList) {
  65. this[kHeadersMap] = new Map(init[kHeadersMap])
  66. this[kHeadersSortedMap] = init[kHeadersSortedMap]
  67. this.cookies = init.cookies
  68. } else {
  69. this[kHeadersMap] = new Map(init)
  70. this[kHeadersSortedMap] = null
  71. }
  72. }
  73. // https://fetch.spec.whatwg.org/#header-list-contains
  74. contains (name) {
  75. // A header list list contains a header name name if list
  76. // contains a header whose name is a byte-case-insensitive
  77. // match for name.
  78. name = name.toLowerCase()
  79. return this[kHeadersMap].has(name)
  80. }
  81. clear () {
  82. this[kHeadersMap].clear()
  83. this[kHeadersSortedMap] = null
  84. this.cookies = null
  85. }
  86. // https://fetch.spec.whatwg.org/#concept-header-list-append
  87. append (name, value) {
  88. this[kHeadersSortedMap] = null
  89. // 1. If list contains name, then set name to the first such
  90. // header’s name.
  91. const lowercaseName = name.toLowerCase()
  92. const exists = this[kHeadersMap].get(lowercaseName)
  93. // 2. Append (name, value) to list.
  94. if (exists) {
  95. const delimiter = lowercaseName === 'cookie' ? '; ' : ', '
  96. this[kHeadersMap].set(lowercaseName, {
  97. name: exists.name,
  98. value: `${exists.value}${delimiter}${value}`
  99. })
  100. } else {
  101. this[kHeadersMap].set(lowercaseName, { name, value })
  102. }
  103. if (lowercaseName === 'set-cookie') {
  104. this.cookies ??= []
  105. this.cookies.push(value)
  106. }
  107. }
  108. // https://fetch.spec.whatwg.org/#concept-header-list-set
  109. set (name, value) {
  110. this[kHeadersSortedMap] = null
  111. const lowercaseName = name.toLowerCase()
  112. if (lowercaseName === 'set-cookie') {
  113. this.cookies = [value]
  114. }
  115. // 1. If list contains name, then set the value of
  116. // the first such header to value and remove the
  117. // others.
  118. // 2. Otherwise, append header (name, value) to list.
  119. return this[kHeadersMap].set(lowercaseName, { name, value })
  120. }
  121. // https://fetch.spec.whatwg.org/#concept-header-list-delete
  122. delete (name) {
  123. this[kHeadersSortedMap] = null
  124. name = name.toLowerCase()
  125. if (name === 'set-cookie') {
  126. this.cookies = null
  127. }
  128. return this[kHeadersMap].delete(name)
  129. }
  130. // https://fetch.spec.whatwg.org/#concept-header-list-get
  131. get (name) {
  132. // 1. If list does not contain name, then return null.
  133. if (!this.contains(name)) {
  134. return null
  135. }
  136. // 2. Return the values of all headers in list whose name
  137. // is a byte-case-insensitive match for name,
  138. // separated from each other by 0x2C 0x20, in order.
  139. return this[kHeadersMap].get(name.toLowerCase())?.value ?? null
  140. }
  141. * [Symbol.iterator] () {
  142. // use the lowercased name
  143. for (const [name, { value }] of this[kHeadersMap]) {
  144. yield [name, value]
  145. }
  146. }
  147. get entries () {
  148. const headers = {}
  149. if (this[kHeadersMap].size) {
  150. for (const { name, value } of this[kHeadersMap].values()) {
  151. headers[name] = value
  152. }
  153. }
  154. return headers
  155. }
  156. }
  157. // https://fetch.spec.whatwg.org/#headers-class
  158. class Headers {
  159. constructor (init = undefined) {
  160. this[kHeadersList] = new HeadersList()
  161. // The new Headers(init) constructor steps are:
  162. // 1. Set this’s guard to "none".
  163. this[kGuard] = 'none'
  164. // 2. If init is given, then fill this with init.
  165. if (init !== undefined) {
  166. init = webidl.converters.HeadersInit(init)
  167. fill(this, init)
  168. }
  169. }
  170. // https://fetch.spec.whatwg.org/#dom-headers-append
  171. append (name, value) {
  172. webidl.brandCheck(this, Headers)
  173. webidl.argumentLengthCheck(arguments, 2, { header: 'Headers.append' })
  174. name = webidl.converters.ByteString(name)
  175. value = webidl.converters.ByteString(value)
  176. // 1. Normalize value.
  177. value = headerValueNormalize(value)
  178. // 2. If name is not a header name or value is not a
  179. // header value, then throw a TypeError.
  180. if (!isValidHeaderName(name)) {
  181. throw webidl.errors.invalidArgument({
  182. prefix: 'Headers.append',
  183. value: name,
  184. type: 'header name'
  185. })
  186. } else if (!isValidHeaderValue(value)) {
  187. throw webidl.errors.invalidArgument({
  188. prefix: 'Headers.append',
  189. value,
  190. type: 'header value'
  191. })
  192. }
  193. // 3. If headers’s guard is "immutable", then throw a TypeError.
  194. // 4. Otherwise, if headers’s guard is "request" and name is a
  195. // forbidden header name, return.
  196. // Note: undici does not implement forbidden header names
  197. if (this[kGuard] === 'immutable') {
  198. throw new TypeError('immutable')
  199. } else if (this[kGuard] === 'request-no-cors') {
  200. // 5. Otherwise, if headers’s guard is "request-no-cors":
  201. // TODO
  202. }
  203. // 6. Otherwise, if headers’s guard is "response" and name is a
  204. // forbidden response-header name, return.
  205. // 7. Append (name, value) to headers’s header list.
  206. // 8. If headers’s guard is "request-no-cors", then remove
  207. // privileged no-CORS request headers from headers
  208. return this[kHeadersList].append(name, value)
  209. }
  210. // https://fetch.spec.whatwg.org/#dom-headers-delete
  211. delete (name) {
  212. webidl.brandCheck(this, Headers)
  213. webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.delete' })
  214. name = webidl.converters.ByteString(name)
  215. // 1. If name is not a header name, then throw a TypeError.
  216. if (!isValidHeaderName(name)) {
  217. throw webidl.errors.invalidArgument({
  218. prefix: 'Headers.delete',
  219. value: name,
  220. type: 'header name'
  221. })
  222. }
  223. // 2. If this’s guard is "immutable", then throw a TypeError.
  224. // 3. Otherwise, if this’s guard is "request" and name is a
  225. // forbidden header name, return.
  226. // 4. Otherwise, if this’s guard is "request-no-cors", name
  227. // is not a no-CORS-safelisted request-header name, and
  228. // name is not a privileged no-CORS request-header name,
  229. // return.
  230. // 5. Otherwise, if this’s guard is "response" and name is
  231. // a forbidden response-header name, return.
  232. // Note: undici does not implement forbidden header names
  233. if (this[kGuard] === 'immutable') {
  234. throw new TypeError('immutable')
  235. } else if (this[kGuard] === 'request-no-cors') {
  236. // TODO
  237. }
  238. // 6. If this’s header list does not contain name, then
  239. // return.
  240. if (!this[kHeadersList].contains(name)) {
  241. return
  242. }
  243. // 7. Delete name from this’s header list.
  244. // 8. If this’s guard is "request-no-cors", then remove
  245. // privileged no-CORS request headers from this.
  246. return this[kHeadersList].delete(name)
  247. }
  248. // https://fetch.spec.whatwg.org/#dom-headers-get
  249. get (name) {
  250. webidl.brandCheck(this, Headers)
  251. webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.get' })
  252. name = webidl.converters.ByteString(name)
  253. // 1. If name is not a header name, then throw a TypeError.
  254. if (!isValidHeaderName(name)) {
  255. throw webidl.errors.invalidArgument({
  256. prefix: 'Headers.get',
  257. value: name,
  258. type: 'header name'
  259. })
  260. }
  261. // 2. Return the result of getting name from this’s header
  262. // list.
  263. return this[kHeadersList].get(name)
  264. }
  265. // https://fetch.spec.whatwg.org/#dom-headers-has
  266. has (name) {
  267. webidl.brandCheck(this, Headers)
  268. webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.has' })
  269. name = webidl.converters.ByteString(name)
  270. // 1. If name is not a header name, then throw a TypeError.
  271. if (!isValidHeaderName(name)) {
  272. throw webidl.errors.invalidArgument({
  273. prefix: 'Headers.has',
  274. value: name,
  275. type: 'header name'
  276. })
  277. }
  278. // 2. Return true if this’s header list contains name;
  279. // otherwise false.
  280. return this[kHeadersList].contains(name)
  281. }
  282. // https://fetch.spec.whatwg.org/#dom-headers-set
  283. set (name, value) {
  284. webidl.brandCheck(this, Headers)
  285. webidl.argumentLengthCheck(arguments, 2, { header: 'Headers.set' })
  286. name = webidl.converters.ByteString(name)
  287. value = webidl.converters.ByteString(value)
  288. // 1. Normalize value.
  289. value = headerValueNormalize(value)
  290. // 2. If name is not a header name or value is not a
  291. // header value, then throw a TypeError.
  292. if (!isValidHeaderName(name)) {
  293. throw webidl.errors.invalidArgument({
  294. prefix: 'Headers.set',
  295. value: name,
  296. type: 'header name'
  297. })
  298. } else if (!isValidHeaderValue(value)) {
  299. throw webidl.errors.invalidArgument({
  300. prefix: 'Headers.set',
  301. value,
  302. type: 'header value'
  303. })
  304. }
  305. // 3. If this’s guard is "immutable", then throw a TypeError.
  306. // 4. Otherwise, if this’s guard is "request" and name is a
  307. // forbidden header name, return.
  308. // 5. Otherwise, if this’s guard is "request-no-cors" and
  309. // name/value is not a no-CORS-safelisted request-header,
  310. // return.
  311. // 6. Otherwise, if this’s guard is "response" and name is a
  312. // forbidden response-header name, return.
  313. // Note: undici does not implement forbidden header names
  314. if (this[kGuard] === 'immutable') {
  315. throw new TypeError('immutable')
  316. } else if (this[kGuard] === 'request-no-cors') {
  317. // TODO
  318. }
  319. // 7. Set (name, value) in this’s header list.
  320. // 8. If this’s guard is "request-no-cors", then remove
  321. // privileged no-CORS request headers from this
  322. return this[kHeadersList].set(name, value)
  323. }
  324. // https://fetch.spec.whatwg.org/#dom-headers-getsetcookie
  325. getSetCookie () {
  326. webidl.brandCheck(this, Headers)
  327. // 1. If this’s header list does not contain `Set-Cookie`, then return « ».
  328. // 2. Return the values of all headers in this’s header list whose name is
  329. // a byte-case-insensitive match for `Set-Cookie`, in order.
  330. const list = this[kHeadersList].cookies
  331. if (list) {
  332. return [...list]
  333. }
  334. return []
  335. }
  336. // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
  337. get [kHeadersSortedMap] () {
  338. if (this[kHeadersList][kHeadersSortedMap]) {
  339. return this[kHeadersList][kHeadersSortedMap]
  340. }
  341. // 1. Let headers be an empty list of headers with the key being the name
  342. // and value the value.
  343. const headers = []
  344. // 2. Let names be the result of convert header names to a sorted-lowercase
  345. // set with all the names of the headers in list.
  346. const names = [...this[kHeadersList]].sort((a, b) => a[0] < b[0] ? -1 : 1)
  347. const cookies = this[kHeadersList].cookies
  348. // 3. For each name of names:
  349. for (const [name, value] of names) {
  350. // 1. If name is `set-cookie`, then:
  351. if (name === 'set-cookie') {
  352. // 1. Let values be a list of all values of headers in list whose name
  353. // is a byte-case-insensitive match for name, in order.
  354. // 2. For each value of values:
  355. // 1. Append (name, value) to headers.
  356. for (const value of cookies) {
  357. headers.push([name, value])
  358. }
  359. } else {
  360. // 2. Otherwise:
  361. // 1. Let value be the result of getting name from list.
  362. // 2. Assert: value is non-null.
  363. assert(value !== null)
  364. // 3. Append (name, value) to headers.
  365. headers.push([name, value])
  366. }
  367. }
  368. this[kHeadersList][kHeadersSortedMap] = headers
  369. // 4. Return headers.
  370. return headers
  371. }
  372. keys () {
  373. webidl.brandCheck(this, Headers)
  374. return makeIterator(
  375. () => [...this[kHeadersSortedMap].values()],
  376. 'Headers',
  377. 'key'
  378. )
  379. }
  380. values () {
  381. webidl.brandCheck(this, Headers)
  382. return makeIterator(
  383. () => [...this[kHeadersSortedMap].values()],
  384. 'Headers',
  385. 'value'
  386. )
  387. }
  388. entries () {
  389. webidl.brandCheck(this, Headers)
  390. return makeIterator(
  391. () => [...this[kHeadersSortedMap].values()],
  392. 'Headers',
  393. 'key+value'
  394. )
  395. }
  396. /**
  397. * @param {(value: string, key: string, self: Headers) => void} callbackFn
  398. * @param {unknown} thisArg
  399. */
  400. forEach (callbackFn, thisArg = globalThis) {
  401. webidl.brandCheck(this, Headers)
  402. webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.forEach' })
  403. if (typeof callbackFn !== 'function') {
  404. throw new TypeError(
  405. "Failed to execute 'forEach' on 'Headers': parameter 1 is not of type 'Function'."
  406. )
  407. }
  408. for (const [key, value] of this) {
  409. callbackFn.apply(thisArg, [value, key, this])
  410. }
  411. }
  412. [Symbol.for('nodejs.util.inspect.custom')] () {
  413. webidl.brandCheck(this, Headers)
  414. return this[kHeadersList]
  415. }
  416. }
  417. Headers.prototype[Symbol.iterator] = Headers.prototype.entries
  418. Object.defineProperties(Headers.prototype, {
  419. append: kEnumerableProperty,
  420. delete: kEnumerableProperty,
  421. get: kEnumerableProperty,
  422. has: kEnumerableProperty,
  423. set: kEnumerableProperty,
  424. getSetCookie: kEnumerableProperty,
  425. keys: kEnumerableProperty,
  426. values: kEnumerableProperty,
  427. entries: kEnumerableProperty,
  428. forEach: kEnumerableProperty,
  429. [Symbol.iterator]: { enumerable: false },
  430. [Symbol.toStringTag]: {
  431. value: 'Headers',
  432. configurable: true
  433. }
  434. })
  435. webidl.converters.HeadersInit = function (V) {
  436. if (webidl.util.Type(V) === 'Object') {
  437. if (V[Symbol.iterator]) {
  438. return webidl.converters['sequence<sequence<ByteString>>'](V)
  439. }
  440. return webidl.converters['record<ByteString, ByteString>'](V)
  441. }
  442. throw webidl.errors.conversionFailed({
  443. prefix: 'Headers constructor',
  444. argument: 'Argument 1',
  445. types: ['sequence<sequence<ByteString>>', 'record<ByteString, ByteString>']
  446. })
  447. }
  448. module.exports = {
  449. fill,
  450. Headers,
  451. HeadersList
  452. }