You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

headers.js 6.4KB

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