headers.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  1. // https://github.com/Ethan-Arrowood/undici-fetch
  2. 'use strict'
  3. const { kConstruct } = require('../../core/symbols')
  4. const { kEnumerableProperty } = require('../../core/util')
  5. const {
  6. iteratorMixin,
  7. isValidHeaderName,
  8. isValidHeaderValue
  9. } = require('./util')
  10. const { webidl } = require('./webidl')
  11. const assert = require('node:assert')
  12. const util = require('node:util')
  13. const kHeadersMap = Symbol('headers map')
  14. const kHeadersSortedMap = Symbol('headers map sorted')
  15. /**
  16. * @param {number} code
  17. */
  18. function isHTTPWhiteSpaceCharCode (code) {
  19. return code === 0x00a || code === 0x00d || code === 0x009 || code === 0x020
  20. }
  21. /**
  22. * @see https://fetch.spec.whatwg.org/#concept-header-value-normalize
  23. * @param {string} potentialValue
  24. */
  25. function headerValueNormalize (potentialValue) {
  26. // To normalize a byte sequence potentialValue, remove
  27. // any leading and trailing HTTP whitespace bytes from
  28. // potentialValue.
  29. let i = 0; let j = potentialValue.length
  30. while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(j - 1))) --j
  31. while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(i))) ++i
  32. return i === 0 && j === potentialValue.length ? potentialValue : potentialValue.substring(i, j)
  33. }
  34. function fill (headers, object) {
  35. // To fill a Headers object headers with a given object object, run these steps:
  36. // 1. If object is a sequence, then for each header in object:
  37. // Note: webidl conversion to array has already been done.
  38. if (Array.isArray(object)) {
  39. for (let i = 0; i < object.length; ++i) {
  40. const header = object[i]
  41. // 1. If header does not contain exactly two items, then throw a TypeError.
  42. if (header.length !== 2) {
  43. throw webidl.errors.exception({
  44. header: 'Headers constructor',
  45. message: `expected name/value pair to be length 2, found ${header.length}.`
  46. })
  47. }
  48. // 2. Append (header’s first item, header’s second item) to headers.
  49. appendHeader(headers, header[0], header[1])
  50. }
  51. } else if (typeof object === 'object' && object !== null) {
  52. // Note: null should throw
  53. // 2. Otherwise, object is a record, then for each key → value in object,
  54. // append (key, value) to headers
  55. const keys = Object.keys(object)
  56. for (let i = 0; i < keys.length; ++i) {
  57. appendHeader(headers, keys[i], object[keys[i]])
  58. }
  59. } else {
  60. throw webidl.errors.conversionFailed({
  61. prefix: 'Headers constructor',
  62. argument: 'Argument 1',
  63. types: ['sequence<sequence<ByteString>>', 'record<ByteString, ByteString>']
  64. })
  65. }
  66. }
  67. /**
  68. * @see https://fetch.spec.whatwg.org/#concept-headers-append
  69. */
  70. function appendHeader (headers, name, value) {
  71. // 1. Normalize value.
  72. value = headerValueNormalize(value)
  73. // 2. If name is not a header name or value is not a
  74. // header value, then throw a TypeError.
  75. if (!isValidHeaderName(name)) {
  76. throw webidl.errors.invalidArgument({
  77. prefix: 'Headers.append',
  78. value: name,
  79. type: 'header name'
  80. })
  81. } else if (!isValidHeaderValue(value)) {
  82. throw webidl.errors.invalidArgument({
  83. prefix: 'Headers.append',
  84. value,
  85. type: 'header value'
  86. })
  87. }
  88. // 3. If headers’s guard is "immutable", then throw a TypeError.
  89. // 4. Otherwise, if headers’s guard is "request" and name is a
  90. // forbidden header name, return.
  91. // 5. Otherwise, if headers’s guard is "request-no-cors":
  92. // TODO
  93. // Note: undici does not implement forbidden header names
  94. if (getHeadersGuard(headers) === 'immutable') {
  95. throw new TypeError('immutable')
  96. }
  97. // 6. Otherwise, if headers’s guard is "response" and name is a
  98. // forbidden response-header name, return.
  99. // 7. Append (name, value) to headers’s header list.
  100. return getHeadersList(headers).append(name, value, false)
  101. // 8. If headers’s guard is "request-no-cors", then remove
  102. // privileged no-CORS request headers from headers
  103. }
  104. function compareHeaderName (a, b) {
  105. return a[0] < b[0] ? -1 : 1
  106. }
  107. class HeadersList {
  108. /** @type {[string, string][]|null} */
  109. cookies = null
  110. constructor (init) {
  111. if (init instanceof HeadersList) {
  112. this[kHeadersMap] = new Map(init[kHeadersMap])
  113. this[kHeadersSortedMap] = init[kHeadersSortedMap]
  114. this.cookies = init.cookies === null ? null : [...init.cookies]
  115. } else {
  116. this[kHeadersMap] = new Map(init)
  117. this[kHeadersSortedMap] = null
  118. }
  119. }
  120. /**
  121. * @see https://fetch.spec.whatwg.org/#header-list-contains
  122. * @param {string} name
  123. * @param {boolean} isLowerCase
  124. */
  125. contains (name, isLowerCase) {
  126. // A header list list contains a header name name if list
  127. // contains a header whose name is a byte-case-insensitive
  128. // match for name.
  129. return this[kHeadersMap].has(isLowerCase ? name : name.toLowerCase())
  130. }
  131. clear () {
  132. this[kHeadersMap].clear()
  133. this[kHeadersSortedMap] = null
  134. this.cookies = null
  135. }
  136. /**
  137. * @see https://fetch.spec.whatwg.org/#concept-header-list-append
  138. * @param {string} name
  139. * @param {string} value
  140. * @param {boolean} isLowerCase
  141. */
  142. append (name, value, isLowerCase) {
  143. this[kHeadersSortedMap] = null
  144. // 1. If list contains name, then set name to the first such
  145. // header’s name.
  146. const lowercaseName = isLowerCase ? name : name.toLowerCase()
  147. const exists = this[kHeadersMap].get(lowercaseName)
  148. // 2. Append (name, value) to list.
  149. if (exists) {
  150. const delimiter = lowercaseName === 'cookie' ? '; ' : ', '
  151. this[kHeadersMap].set(lowercaseName, {
  152. name: exists.name,
  153. value: `${exists.value}${delimiter}${value}`
  154. })
  155. } else {
  156. this[kHeadersMap].set(lowercaseName, { name, value })
  157. }
  158. if (lowercaseName === 'set-cookie') {
  159. (this.cookies ??= []).push(value)
  160. }
  161. }
  162. /**
  163. * @see https://fetch.spec.whatwg.org/#concept-header-list-set
  164. * @param {string} name
  165. * @param {string} value
  166. * @param {boolean} isLowerCase
  167. */
  168. set (name, value, isLowerCase) {
  169. this[kHeadersSortedMap] = null
  170. const lowercaseName = isLowerCase ? name : name.toLowerCase()
  171. if (lowercaseName === 'set-cookie') {
  172. this.cookies = [value]
  173. }
  174. // 1. If list contains name, then set the value of
  175. // the first such header to value and remove the
  176. // others.
  177. // 2. Otherwise, append header (name, value) to list.
  178. this[kHeadersMap].set(lowercaseName, { name, value })
  179. }
  180. /**
  181. * @see https://fetch.spec.whatwg.org/#concept-header-list-delete
  182. * @param {string} name
  183. * @param {boolean} isLowerCase
  184. */
  185. delete (name, isLowerCase) {
  186. this[kHeadersSortedMap] = null
  187. if (!isLowerCase) name = name.toLowerCase()
  188. if (name === 'set-cookie') {
  189. this.cookies = null
  190. }
  191. this[kHeadersMap].delete(name)
  192. }
  193. /**
  194. * @see https://fetch.spec.whatwg.org/#concept-header-list-get
  195. * @param {string} name
  196. * @param {boolean} isLowerCase
  197. * @returns {string | null}
  198. */
  199. get (name, isLowerCase) {
  200. // 1. If list does not contain name, then return null.
  201. // 2. Return the values of all headers in list whose name
  202. // is a byte-case-insensitive match for name,
  203. // separated from each other by 0x2C 0x20, in order.
  204. return this[kHeadersMap].get(isLowerCase ? name : name.toLowerCase())?.value ?? null
  205. }
  206. * [Symbol.iterator] () {
  207. // use the lowercased name
  208. for (const { 0: name, 1: { value } } of this[kHeadersMap]) {
  209. yield [name, value]
  210. }
  211. }
  212. get entries () {
  213. const headers = {}
  214. if (this[kHeadersMap].size !== 0) {
  215. for (const { name, value } of this[kHeadersMap].values()) {
  216. headers[name] = value
  217. }
  218. }
  219. return headers
  220. }
  221. rawValues () {
  222. return this[kHeadersMap].values()
  223. }
  224. get entriesList () {
  225. const headers = []
  226. if (this[kHeadersMap].size !== 0) {
  227. for (const { 0: lowerName, 1: { name, value } } of this[kHeadersMap]) {
  228. if (lowerName === 'set-cookie') {
  229. for (const cookie of this.cookies) {
  230. headers.push([name, cookie])
  231. }
  232. } else {
  233. headers.push([name, value])
  234. }
  235. }
  236. }
  237. return headers
  238. }
  239. // https://fetch.spec.whatwg.org/#convert-header-names-to-a-sorted-lowercase-set
  240. toSortedArray () {
  241. const size = this[kHeadersMap].size
  242. const array = new Array(size)
  243. // In most cases, you will use the fast-path.
  244. // fast-path: Use binary insertion sort for small arrays.
  245. if (size <= 32) {
  246. if (size === 0) {
  247. // If empty, it is an empty array. To avoid the first index assignment.
  248. return array
  249. }
  250. // Improve performance by unrolling loop and avoiding double-loop.
  251. // Double-loop-less version of the binary insertion sort.
  252. const iterator = this[kHeadersMap][Symbol.iterator]()
  253. const firstValue = iterator.next().value
  254. // set [name, value] to first index.
  255. array[0] = [firstValue[0], firstValue[1].value]
  256. // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
  257. // 3.2.2. Assert: value is non-null.
  258. assert(firstValue[1].value !== null)
  259. for (
  260. let i = 1, j = 0, right = 0, left = 0, pivot = 0, x, value;
  261. i < size;
  262. ++i
  263. ) {
  264. // get next value
  265. value = iterator.next().value
  266. // set [name, value] to current index.
  267. x = array[i] = [value[0], value[1].value]
  268. // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
  269. // 3.2.2. Assert: value is non-null.
  270. assert(x[1] !== null)
  271. left = 0
  272. right = i
  273. // binary search
  274. while (left < right) {
  275. // middle index
  276. pivot = left + ((right - left) >> 1)
  277. // compare header name
  278. if (array[pivot][0] <= x[0]) {
  279. left = pivot + 1
  280. } else {
  281. right = pivot
  282. }
  283. }
  284. if (i !== pivot) {
  285. j = i
  286. while (j > left) {
  287. array[j] = array[--j]
  288. }
  289. array[left] = x
  290. }
  291. }
  292. /* c8 ignore next 4 */
  293. if (!iterator.next().done) {
  294. // This is for debugging and will never be called.
  295. throw new TypeError('Unreachable')
  296. }
  297. return array
  298. } else {
  299. // This case would be a rare occurrence.
  300. // slow-path: fallback
  301. let i = 0
  302. for (const { 0: name, 1: { value } } of this[kHeadersMap]) {
  303. array[i++] = [name, value]
  304. // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
  305. // 3.2.2. Assert: value is non-null.
  306. assert(value !== null)
  307. }
  308. return array.sort(compareHeaderName)
  309. }
  310. }
  311. }
  312. // https://fetch.spec.whatwg.org/#headers-class
  313. class Headers {
  314. #guard
  315. #headersList
  316. constructor (init = undefined) {
  317. webidl.util.markAsUncloneable(this)
  318. if (init === kConstruct) {
  319. return
  320. }
  321. this.#headersList = new HeadersList()
  322. // The new Headers(init) constructor steps are:
  323. // 1. Set this’s guard to "none".
  324. this.#guard = 'none'
  325. // 2. If init is given, then fill this with init.
  326. if (init !== undefined) {
  327. init = webidl.converters.HeadersInit(init, 'Headers contructor', 'init')
  328. fill(this, init)
  329. }
  330. }
  331. // https://fetch.spec.whatwg.org/#dom-headers-append
  332. append (name, value) {
  333. webidl.brandCheck(this, Headers)
  334. webidl.argumentLengthCheck(arguments, 2, 'Headers.append')
  335. const prefix = 'Headers.append'
  336. name = webidl.converters.ByteString(name, prefix, 'name')
  337. value = webidl.converters.ByteString(value, prefix, 'value')
  338. return appendHeader(this, name, value)
  339. }
  340. // https://fetch.spec.whatwg.org/#dom-headers-delete
  341. delete (name) {
  342. webidl.brandCheck(this, Headers)
  343. webidl.argumentLengthCheck(arguments, 1, 'Headers.delete')
  344. const prefix = 'Headers.delete'
  345. name = webidl.converters.ByteString(name, prefix, 'name')
  346. // 1. If name is not a header name, then throw a TypeError.
  347. if (!isValidHeaderName(name)) {
  348. throw webidl.errors.invalidArgument({
  349. prefix: 'Headers.delete',
  350. value: name,
  351. type: 'header name'
  352. })
  353. }
  354. // 2. If this’s guard is "immutable", then throw a TypeError.
  355. // 3. Otherwise, if this’s guard is "request" and name is a
  356. // forbidden header name, return.
  357. // 4. Otherwise, if this’s guard is "request-no-cors", name
  358. // is not a no-CORS-safelisted request-header name, and
  359. // name is not a privileged no-CORS request-header name,
  360. // return.
  361. // 5. Otherwise, if this’s guard is "response" and name is
  362. // a forbidden response-header name, return.
  363. // Note: undici does not implement forbidden header names
  364. if (this.#guard === 'immutable') {
  365. throw new TypeError('immutable')
  366. }
  367. // 6. If this’s header list does not contain name, then
  368. // return.
  369. if (!this.#headersList.contains(name, false)) {
  370. return
  371. }
  372. // 7. Delete name from this’s header list.
  373. // 8. If this’s guard is "request-no-cors", then remove
  374. // privileged no-CORS request headers from this.
  375. this.#headersList.delete(name, false)
  376. }
  377. // https://fetch.spec.whatwg.org/#dom-headers-get
  378. get (name) {
  379. webidl.brandCheck(this, Headers)
  380. webidl.argumentLengthCheck(arguments, 1, 'Headers.get')
  381. const prefix = 'Headers.get'
  382. name = webidl.converters.ByteString(name, prefix, 'name')
  383. // 1. If name is not a header name, then throw a TypeError.
  384. if (!isValidHeaderName(name)) {
  385. throw webidl.errors.invalidArgument({
  386. prefix,
  387. value: name,
  388. type: 'header name'
  389. })
  390. }
  391. // 2. Return the result of getting name from this’s header
  392. // list.
  393. return this.#headersList.get(name, false)
  394. }
  395. // https://fetch.spec.whatwg.org/#dom-headers-has
  396. has (name) {
  397. webidl.brandCheck(this, Headers)
  398. webidl.argumentLengthCheck(arguments, 1, 'Headers.has')
  399. const prefix = 'Headers.has'
  400. name = webidl.converters.ByteString(name, prefix, 'name')
  401. // 1. If name is not a header name, then throw a TypeError.
  402. if (!isValidHeaderName(name)) {
  403. throw webidl.errors.invalidArgument({
  404. prefix,
  405. value: name,
  406. type: 'header name'
  407. })
  408. }
  409. // 2. Return true if this’s header list contains name;
  410. // otherwise false.
  411. return this.#headersList.contains(name, false)
  412. }
  413. // https://fetch.spec.whatwg.org/#dom-headers-set
  414. set (name, value) {
  415. webidl.brandCheck(this, Headers)
  416. webidl.argumentLengthCheck(arguments, 2, 'Headers.set')
  417. const prefix = 'Headers.set'
  418. name = webidl.converters.ByteString(name, prefix, 'name')
  419. value = webidl.converters.ByteString(value, prefix, 'value')
  420. // 1. Normalize value.
  421. value = headerValueNormalize(value)
  422. // 2. If name is not a header name or value is not a
  423. // header value, then throw a TypeError.
  424. if (!isValidHeaderName(name)) {
  425. throw webidl.errors.invalidArgument({
  426. prefix,
  427. value: name,
  428. type: 'header name'
  429. })
  430. } else if (!isValidHeaderValue(value)) {
  431. throw webidl.errors.invalidArgument({
  432. prefix,
  433. value,
  434. type: 'header value'
  435. })
  436. }
  437. // 3. If this’s guard is "immutable", then throw a TypeError.
  438. // 4. Otherwise, if this’s guard is "request" and name is a
  439. // forbidden header name, return.
  440. // 5. Otherwise, if this’s guard is "request-no-cors" and
  441. // name/value is not a no-CORS-safelisted request-header,
  442. // return.
  443. // 6. Otherwise, if this’s guard is "response" and name is a
  444. // forbidden response-header name, return.
  445. // Note: undici does not implement forbidden header names
  446. if (this.#guard === 'immutable') {
  447. throw new TypeError('immutable')
  448. }
  449. // 7. Set (name, value) in this’s header list.
  450. // 8. If this’s guard is "request-no-cors", then remove
  451. // privileged no-CORS request headers from this
  452. this.#headersList.set(name, value, false)
  453. }
  454. // https://fetch.spec.whatwg.org/#dom-headers-getsetcookie
  455. getSetCookie () {
  456. webidl.brandCheck(this, Headers)
  457. // 1. If this’s header list does not contain `Set-Cookie`, then return « ».
  458. // 2. Return the values of all headers in this’s header list whose name is
  459. // a byte-case-insensitive match for `Set-Cookie`, in order.
  460. const list = this.#headersList.cookies
  461. if (list) {
  462. return [...list]
  463. }
  464. return []
  465. }
  466. // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
  467. get [kHeadersSortedMap] () {
  468. if (this.#headersList[kHeadersSortedMap]) {
  469. return this.#headersList[kHeadersSortedMap]
  470. }
  471. // 1. Let headers be an empty list of headers with the key being the name
  472. // and value the value.
  473. const headers = []
  474. // 2. Let names be the result of convert header names to a sorted-lowercase
  475. // set with all the names of the headers in list.
  476. const names = this.#headersList.toSortedArray()
  477. const cookies = this.#headersList.cookies
  478. // fast-path
  479. if (cookies === null || cookies.length === 1) {
  480. // Note: The non-null assertion of value has already been done by `HeadersList#toSortedArray`
  481. return (this.#headersList[kHeadersSortedMap] = names)
  482. }
  483. // 3. For each name of names:
  484. for (let i = 0; i < names.length; ++i) {
  485. const { 0: name, 1: value } = names[i]
  486. // 1. If name is `set-cookie`, then:
  487. if (name === 'set-cookie') {
  488. // 1. Let values be a list of all values of headers in list whose name
  489. // is a byte-case-insensitive match for name, in order.
  490. // 2. For each value of values:
  491. // 1. Append (name, value) to headers.
  492. for (let j = 0; j < cookies.length; ++j) {
  493. headers.push([name, cookies[j]])
  494. }
  495. } else {
  496. // 2. Otherwise:
  497. // 1. Let value be the result of getting name from list.
  498. // 2. Assert: value is non-null.
  499. // Note: This operation was done by `HeadersList#toSortedArray`.
  500. // 3. Append (name, value) to headers.
  501. headers.push([name, value])
  502. }
  503. }
  504. // 4. Return headers.
  505. return (this.#headersList[kHeadersSortedMap] = headers)
  506. }
  507. [util.inspect.custom] (depth, options) {
  508. options.depth ??= depth
  509. return `Headers ${util.formatWithOptions(options, this.#headersList.entries)}`
  510. }
  511. static getHeadersGuard (o) {
  512. return o.#guard
  513. }
  514. static setHeadersGuard (o, guard) {
  515. o.#guard = guard
  516. }
  517. static getHeadersList (o) {
  518. return o.#headersList
  519. }
  520. static setHeadersList (o, list) {
  521. o.#headersList = list
  522. }
  523. }
  524. const { getHeadersGuard, setHeadersGuard, getHeadersList, setHeadersList } = Headers
  525. Reflect.deleteProperty(Headers, 'getHeadersGuard')
  526. Reflect.deleteProperty(Headers, 'setHeadersGuard')
  527. Reflect.deleteProperty(Headers, 'getHeadersList')
  528. Reflect.deleteProperty(Headers, 'setHeadersList')
  529. iteratorMixin('Headers', Headers, kHeadersSortedMap, 0, 1)
  530. Object.defineProperties(Headers.prototype, {
  531. append: kEnumerableProperty,
  532. delete: kEnumerableProperty,
  533. get: kEnumerableProperty,
  534. has: kEnumerableProperty,
  535. set: kEnumerableProperty,
  536. getSetCookie: kEnumerableProperty,
  537. [Symbol.toStringTag]: {
  538. value: 'Headers',
  539. configurable: true
  540. },
  541. [util.inspect.custom]: {
  542. enumerable: false
  543. }
  544. })
  545. webidl.converters.HeadersInit = function (V, prefix, argument) {
  546. if (webidl.util.Type(V) === 'Object') {
  547. const iterator = Reflect.get(V, Symbol.iterator)
  548. // A work-around to ensure we send the properly-cased Headers when V is a Headers object.
  549. // Read https://github.com/nodejs/undici/pull/3159#issuecomment-2075537226 before touching, please.
  550. if (!util.types.isProxy(V) && iterator === Headers.prototype.entries) { // Headers object
  551. try {
  552. return getHeadersList(V).entriesList
  553. } catch {
  554. // fall-through
  555. }
  556. }
  557. if (typeof iterator === 'function') {
  558. return webidl.converters['sequence<sequence<ByteString>>'](V, prefix, argument, iterator.bind(V))
  559. }
  560. return webidl.converters['record<ByteString, ByteString>'](V, prefix, argument)
  561. }
  562. throw webidl.errors.conversionFailed({
  563. prefix: 'Headers constructor',
  564. argument: 'Argument 1',
  565. types: ['sequence<sequence<ByteString>>', 'record<ByteString, ByteString>']
  566. })
  567. }
  568. module.exports = {
  569. fill,
  570. // for test.
  571. compareHeaderName,
  572. Headers,
  573. HeadersList,
  574. getHeadersGuard,
  575. setHeadersGuard,
  576. setHeadersList,
  577. getHeadersList
  578. }