headers.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. 'use strict'
  2. const invalidTokenRegex = /[^\^_`a-zA-Z\-0-9!#$%&'*+.|~]/
  3. const invalidHeaderCharRegex = /[^\t\x20-\x7e\x80-\xff]/
  4. const validateName = name => {
  5. name = `${name}`
  6. if (invalidTokenRegex.test(name) || name === '')
  7. throw new TypeError(`${name} is not a legal HTTP header name`)
  8. }
  9. const validateValue = value => {
  10. value = `${value}`
  11. if (invalidHeaderCharRegex.test(value))
  12. throw new TypeError(`${value} is not a legal HTTP header value`)
  13. }
  14. const find = (map, name) => {
  15. name = name.toLowerCase()
  16. for (const key in map) {
  17. if (key.toLowerCase() === name)
  18. return key
  19. }
  20. return undefined
  21. }
  22. const MAP = Symbol('map')
  23. class Headers {
  24. constructor (init = undefined) {
  25. this[MAP] = Object.create(null)
  26. if (init instanceof Headers) {
  27. const rawHeaders = init.raw()
  28. const headerNames = Object.keys(rawHeaders)
  29. for (const headerName of headerNames) {
  30. for (const value of rawHeaders[headerName]) {
  31. this.append(headerName, value)
  32. }
  33. }
  34. return
  35. }
  36. // no-op
  37. if (init === undefined || init === null)
  38. return
  39. if (typeof init === 'object') {
  40. const method = init[Symbol.iterator]
  41. if (method !== null && method !== undefined) {
  42. if (typeof method !== 'function')
  43. throw new TypeError('Header pairs must be iterable')
  44. // sequence<sequence<ByteString>>
  45. // Note: per spec we have to first exhaust the lists then process them
  46. const pairs = []
  47. for (const pair of init) {
  48. if (typeof pair !== 'object' ||
  49. typeof pair[Symbol.iterator] !== 'function')
  50. throw new TypeError('Each header pair must be iterable')
  51. const arrPair = Array.from(pair)
  52. if (arrPair.length !== 2)
  53. throw new TypeError('Each header pair must be a name/value tuple')
  54. pairs.push(arrPair)
  55. }
  56. for (const pair of pairs) {
  57. this.append(pair[0], pair[1])
  58. }
  59. } else {
  60. // record<ByteString, ByteString>
  61. for (const key of Object.keys(init)) {
  62. this.append(key, init[key])
  63. }
  64. }
  65. } else
  66. throw new TypeError('Provided initializer must be an object')
  67. }
  68. get (name) {
  69. name = `${name}`
  70. validateName(name)
  71. const key = find(this[MAP], name)
  72. if (key === undefined)
  73. return null
  74. return this[MAP][key].join(', ')
  75. }
  76. forEach (callback, thisArg = undefined) {
  77. let pairs = getHeaders(this)
  78. for (let i = 0; i < pairs.length; i++) {
  79. const [name, value] = pairs[i]
  80. callback.call(thisArg, value, name, this)
  81. // refresh in case the callback added more headers
  82. pairs = getHeaders(this)
  83. }
  84. }
  85. set (name, value) {
  86. name = `${name}`
  87. value = `${value}`
  88. validateName(name)
  89. validateValue(value)
  90. const key = find(this[MAP], name)
  91. this[MAP][key !== undefined ? key : name] = [value]
  92. }
  93. append (name, value) {
  94. name = `${name}`
  95. value = `${value}`
  96. validateName(name)
  97. validateValue(value)
  98. const key = find(this[MAP], name)
  99. if (key !== undefined)
  100. this[MAP][key].push(value)
  101. else
  102. this[MAP][name] = [value]
  103. }
  104. has (name) {
  105. name = `${name}`
  106. validateName(name)
  107. return find(this[MAP], name) !== undefined
  108. }
  109. delete (name) {
  110. name = `${name}`
  111. validateName(name)
  112. const key = find(this[MAP], name)
  113. if (key !== undefined)
  114. delete this[MAP][key]
  115. }
  116. raw () {
  117. return this[MAP]
  118. }
  119. keys () {
  120. return new HeadersIterator(this, 'key')
  121. }
  122. values () {
  123. return new HeadersIterator(this, 'value')
  124. }
  125. [Symbol.iterator]() {
  126. return new HeadersIterator(this, 'key+value')
  127. }
  128. entries () {
  129. return new HeadersIterator(this, 'key+value')
  130. }
  131. get [Symbol.toStringTag] () {
  132. return 'Headers'
  133. }
  134. static exportNodeCompatibleHeaders (headers) {
  135. const obj = Object.assign(Object.create(null), headers[MAP])
  136. // http.request() only supports string as Host header. This hack makes
  137. // specifying custom Host header possible.
  138. const hostHeaderKey = find(headers[MAP], 'Host')
  139. if (hostHeaderKey !== undefined)
  140. obj[hostHeaderKey] = obj[hostHeaderKey][0]
  141. return obj
  142. }
  143. static createHeadersLenient (obj) {
  144. const headers = new Headers()
  145. for (const name of Object.keys(obj)) {
  146. if (invalidTokenRegex.test(name))
  147. continue
  148. if (Array.isArray(obj[name])) {
  149. for (const val of obj[name]) {
  150. if (invalidHeaderCharRegex.test(val))
  151. continue
  152. if (headers[MAP][name] === undefined)
  153. headers[MAP][name] = [val]
  154. else
  155. headers[MAP][name].push(val)
  156. }
  157. } else if (!invalidHeaderCharRegex.test(obj[name]))
  158. headers[MAP][name] = [obj[name]]
  159. }
  160. return headers
  161. }
  162. }
  163. Object.defineProperties(Headers.prototype, {
  164. get: { enumerable: true },
  165. forEach: { enumerable: true },
  166. set: { enumerable: true },
  167. append: { enumerable: true },
  168. has: { enumerable: true },
  169. delete: { enumerable: true },
  170. keys: { enumerable: true },
  171. values: { enumerable: true },
  172. entries: { enumerable: true },
  173. })
  174. const getHeaders = (headers, kind = 'key+value') =>
  175. Object.keys(headers[MAP]).sort().map(
  176. kind === 'key' ? k => k.toLowerCase()
  177. : kind === 'value' ? k => headers[MAP][k].join(', ')
  178. : k => [k.toLowerCase(), headers[MAP][k].join(', ')]
  179. )
  180. const INTERNAL = Symbol('internal')
  181. class HeadersIterator {
  182. constructor (target, kind) {
  183. this[INTERNAL] = {
  184. target,
  185. kind,
  186. index: 0,
  187. }
  188. }
  189. get [Symbol.toStringTag] () {
  190. return 'HeadersIterator'
  191. }
  192. next () {
  193. /* istanbul ignore if: should be impossible */
  194. if (!this || Object.getPrototypeOf(this) !== HeadersIterator.prototype)
  195. throw new TypeError('Value of `this` is not a HeadersIterator')
  196. const { target, kind, index } = this[INTERNAL]
  197. const values = getHeaders(target, kind)
  198. const len = values.length
  199. if (index >= len) {
  200. return {
  201. value: undefined,
  202. done: true,
  203. }
  204. }
  205. this[INTERNAL].index++
  206. return { value: values[index], done: false }
  207. }
  208. }
  209. // manually extend because 'extends' requires a ctor
  210. Object.setPrototypeOf(HeadersIterator.prototype,
  211. Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]())))
  212. module.exports = Headers