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