• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict'
2
3/**
4 * headers.js
5 *
6 * Headers class offers convenient helpers
7 */
8
9const common = require('./common.js')
10const checkInvalidHeaderChar = common.checkInvalidHeaderChar
11const checkIsHttpToken = common.checkIsHttpToken
12
13function sanitizeName (name) {
14  name += ''
15  if (!checkIsHttpToken(name)) {
16    throw new TypeError(`${name} is not a legal HTTP header name`)
17  }
18  return name.toLowerCase()
19}
20
21function sanitizeValue (value) {
22  value += ''
23  if (checkInvalidHeaderChar(value)) {
24    throw new TypeError(`${value} is not a legal HTTP header value`)
25  }
26  return value
27}
28
29const MAP = Symbol('map')
30class Headers {
31  /**
32   * Headers class
33   *
34   * @param   Object  headers  Response headers
35   * @return  Void
36   */
37  constructor (init) {
38    this[MAP] = Object.create(null)
39
40    if (init instanceof Headers) {
41      const rawHeaders = init.raw()
42      const headerNames = Object.keys(rawHeaders)
43
44      for (const headerName of headerNames) {
45        for (const value of rawHeaders[headerName]) {
46          this.append(headerName, value)
47        }
48      }
49
50      return
51    }
52
53    // We don't worry about converting prop to ByteString here as append()
54    // will handle it.
55    if (init == null) {
56      // no op
57    } else if (typeof init === 'object') {
58      const method = init[Symbol.iterator]
59      if (method != null) {
60        if (typeof method !== 'function') {
61          throw new TypeError('Header pairs must be iterable')
62        }
63
64        // sequence<sequence<ByteString>>
65        // Note: per spec we have to first exhaust the lists then process them
66        const pairs = []
67        for (const pair of init) {
68          if (typeof pair !== 'object' || typeof pair[Symbol.iterator] !== 'function') {
69            throw new TypeError('Each header pair must be iterable')
70          }
71          pairs.push(Array.from(pair))
72        }
73
74        for (const pair of pairs) {
75          if (pair.length !== 2) {
76            throw new TypeError('Each header pair must be a name/value tuple')
77          }
78          this.append(pair[0], pair[1])
79        }
80      } else {
81        // record<ByteString, ByteString>
82        for (const key of Object.keys(init)) {
83          const value = init[key]
84          this.append(key, value)
85        }
86      }
87    } else {
88      throw new TypeError('Provided initializer must be an object')
89    }
90
91    Object.defineProperty(this, Symbol.toStringTag, {
92      value: 'Headers',
93      writable: false,
94      enumerable: false,
95      configurable: true
96    })
97  }
98
99  /**
100   * Return first header value given name
101   *
102   * @param   String  name  Header name
103   * @return  Mixed
104   */
105  get (name) {
106    const list = this[MAP][sanitizeName(name)]
107    if (!list) {
108      return null
109    }
110
111    return list.join(', ')
112  }
113
114  /**
115   * Iterate over all headers
116   *
117   * @param   Function  callback  Executed for each item with parameters (value, name, thisArg)
118   * @param   Boolean   thisArg   `this` context for callback function
119   * @return  Void
120   */
121  forEach (callback, thisArg) {
122    let pairs = getHeaderPairs(this)
123    let i = 0
124    while (i < pairs.length) {
125      const name = pairs[i][0]
126      const value = pairs[i][1]
127      callback.call(thisArg, value, name, this)
128      pairs = getHeaderPairs(this)
129      i++
130    }
131  }
132
133  /**
134   * Overwrite header values given name
135   *
136   * @param   String  name   Header name
137   * @param   String  value  Header value
138   * @return  Void
139   */
140  set (name, value) {
141    this[MAP][sanitizeName(name)] = [sanitizeValue(value)]
142  }
143
144  /**
145   * Append a value onto existing header
146   *
147   * @param   String  name   Header name
148   * @param   String  value  Header value
149   * @return  Void
150   */
151  append (name, value) {
152    if (!this.has(name)) {
153      this.set(name, value)
154      return
155    }
156
157    this[MAP][sanitizeName(name)].push(sanitizeValue(value))
158  }
159
160  /**
161   * Check for header name existence
162   *
163   * @param   String   name  Header name
164   * @return  Boolean
165   */
166  has (name) {
167    return !!this[MAP][sanitizeName(name)]
168  }
169
170  /**
171   * Delete all header values given name
172   *
173   * @param   String  name  Header name
174   * @return  Void
175   */
176  delete (name) {
177    delete this[MAP][sanitizeName(name)]
178  };
179
180  /**
181   * Return raw headers (non-spec api)
182   *
183   * @return  Object
184   */
185  raw () {
186    return this[MAP]
187  }
188
189  /**
190   * Get an iterator on keys.
191   *
192   * @return  Iterator
193   */
194  keys () {
195    return createHeadersIterator(this, 'key')
196  }
197
198  /**
199   * Get an iterator on values.
200   *
201   * @return  Iterator
202   */
203  values () {
204    return createHeadersIterator(this, 'value')
205  }
206
207  /**
208   * Get an iterator on entries.
209   *
210   * This is the default iterator of the Headers object.
211   *
212   * @return  Iterator
213   */
214  [Symbol.iterator] () {
215    return createHeadersIterator(this, 'key+value')
216  }
217}
218Headers.prototype.entries = Headers.prototype[Symbol.iterator]
219
220Object.defineProperty(Headers.prototype, Symbol.toStringTag, {
221  value: 'HeadersPrototype',
222  writable: false,
223  enumerable: false,
224  configurable: true
225})
226
227function getHeaderPairs (headers, kind) {
228  const keys = Object.keys(headers[MAP]).sort()
229  return keys.map(
230    kind === 'key'
231      ? k => [k]
232      : k => [k, headers.get(k)]
233  )
234}
235
236const INTERNAL = Symbol('internal')
237
238function createHeadersIterator (target, kind) {
239  const iterator = Object.create(HeadersIteratorPrototype)
240  iterator[INTERNAL] = {
241    target,
242    kind,
243    index: 0
244  }
245  return iterator
246}
247
248const HeadersIteratorPrototype = Object.setPrototypeOf({
249  next () {
250    // istanbul ignore if
251    if (!this ||
252      Object.getPrototypeOf(this) !== HeadersIteratorPrototype) {
253      throw new TypeError('Value of `this` is not a HeadersIterator')
254    }
255
256    const target = this[INTERNAL].target
257    const kind = this[INTERNAL].kind
258    const index = this[INTERNAL].index
259    const values = getHeaderPairs(target, kind)
260    const len = values.length
261    if (index >= len) {
262      return {
263        value: undefined,
264        done: true
265      }
266    }
267
268    const pair = values[index]
269    this[INTERNAL].index = index + 1
270
271    let result
272    if (kind === 'key') {
273      result = pair[0]
274    } else if (kind === 'value') {
275      result = pair[1]
276    } else {
277      result = pair
278    }
279
280    return {
281      value: result,
282      done: false
283    }
284  }
285}, Object.getPrototypeOf(
286  Object.getPrototypeOf([][Symbol.iterator]())
287))
288
289Object.defineProperty(HeadersIteratorPrototype, Symbol.toStringTag, {
290  value: 'HeadersIterator',
291  writable: false,
292  enumerable: false,
293  configurable: true
294})
295
296module.exports = Headers
297