/ node_modules / send / index.js
index.js
  1  /*!
  2   * send
  3   * Copyright(c) 2012 TJ Holowaychuk
  4   * Copyright(c) 2014-2022 Douglas Christopher Wilson
  5   * MIT Licensed
  6   */
  7  
  8  'use strict'
  9  
 10  /**
 11   * Module dependencies.
 12   * @private
 13   */
 14  
 15  var createError = require('http-errors')
 16  var debug = require('debug')('send')
 17  var encodeUrl = require('encodeurl')
 18  var escapeHtml = require('escape-html')
 19  var etag = require('etag')
 20  var fresh = require('fresh')
 21  var fs = require('fs')
 22  var mime = require('mime-types')
 23  var ms = require('ms')
 24  var onFinished = require('on-finished')
 25  var parseRange = require('range-parser')
 26  var path = require('path')
 27  var statuses = require('statuses')
 28  var Stream = require('stream')
 29  var util = require('util')
 30  
 31  /**
 32   * Path function references.
 33   * @private
 34   */
 35  
 36  var extname = path.extname
 37  var join = path.join
 38  var normalize = path.normalize
 39  var resolve = path.resolve
 40  var sep = path.sep
 41  
 42  /**
 43   * Regular expression for identifying a bytes Range header.
 44   * @private
 45   */
 46  
 47  var BYTES_RANGE_REGEXP = /^ *bytes=/
 48  
 49  /**
 50   * Maximum value allowed for the max age.
 51   * @private
 52   */
 53  
 54  var MAX_MAXAGE = 60 * 60 * 24 * 365 * 1000 // 1 year
 55  
 56  /**
 57   * Regular expression to match a path with a directory up component.
 58   * @private
 59   */
 60  
 61  var UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/
 62  
 63  /**
 64   * Module exports.
 65   * @public
 66   */
 67  
 68  module.exports = send
 69  
 70  /**
 71   * Return a `SendStream` for `req` and `path`.
 72   *
 73   * @param {object} req
 74   * @param {string} path
 75   * @param {object} [options]
 76   * @return {SendStream}
 77   * @public
 78   */
 79  
 80  function send (req, path, options) {
 81    return new SendStream(req, path, options)
 82  }
 83  
 84  /**
 85   * Initialize a `SendStream` with the given `path`.
 86   *
 87   * @param {Request} req
 88   * @param {String} path
 89   * @param {object} [options]
 90   * @private
 91   */
 92  
 93  function SendStream (req, path, options) {
 94    Stream.call(this)
 95  
 96    var opts = options || {}
 97  
 98    this.options = opts
 99    this.path = path
100    this.req = req
101  
102    this._acceptRanges = opts.acceptRanges !== undefined
103      ? Boolean(opts.acceptRanges)
104      : true
105  
106    this._cacheControl = opts.cacheControl !== undefined
107      ? Boolean(opts.cacheControl)
108      : true
109  
110    this._etag = opts.etag !== undefined
111      ? Boolean(opts.etag)
112      : true
113  
114    this._dotfiles = opts.dotfiles !== undefined
115      ? opts.dotfiles
116      : 'ignore'
117  
118    if (this._dotfiles !== 'ignore' && this._dotfiles !== 'allow' && this._dotfiles !== 'deny') {
119      throw new TypeError('dotfiles option must be "allow", "deny", or "ignore"')
120    }
121  
122    this._extensions = opts.extensions !== undefined
123      ? normalizeList(opts.extensions, 'extensions option')
124      : []
125  
126    this._immutable = opts.immutable !== undefined
127      ? Boolean(opts.immutable)
128      : false
129  
130    this._index = opts.index !== undefined
131      ? normalizeList(opts.index, 'index option')
132      : ['index.html']
133  
134    this._lastModified = opts.lastModified !== undefined
135      ? Boolean(opts.lastModified)
136      : true
137  
138    this._maxage = opts.maxAge || opts.maxage
139    this._maxage = typeof this._maxage === 'string'
140      ? ms(this._maxage)
141      : Number(this._maxage)
142    this._maxage = !isNaN(this._maxage)
143      ? Math.min(Math.max(0, this._maxage), MAX_MAXAGE)
144      : 0
145  
146    this._root = opts.root
147      ? resolve(opts.root)
148      : null
149  }
150  
151  /**
152   * Inherits from `Stream`.
153   */
154  
155  util.inherits(SendStream, Stream)
156  
157  /**
158   * Emit error with `status`.
159   *
160   * @param {number} status
161   * @param {Error} [err]
162   * @private
163   */
164  
165  SendStream.prototype.error = function error (status, err) {
166    // emit if listeners instead of responding
167    if (hasListeners(this, 'error')) {
168      return this.emit('error', createHttpError(status, err))
169    }
170  
171    var res = this.res
172    var msg = statuses.message[status] || String(status)
173    var doc = createHtmlDocument('Error', escapeHtml(msg))
174  
175    // clear existing headers
176    clearHeaders(res)
177  
178    // add error headers
179    if (err && err.headers) {
180      setHeaders(res, err.headers)
181    }
182  
183    // send basic response
184    res.statusCode = status
185    res.setHeader('Content-Type', 'text/html; charset=UTF-8')
186    res.setHeader('Content-Length', Buffer.byteLength(doc))
187    res.setHeader('Content-Security-Policy', "default-src 'none'")
188    res.setHeader('X-Content-Type-Options', 'nosniff')
189    res.end(doc)
190  }
191  
192  /**
193   * Check if the pathname ends with "/".
194   *
195   * @return {boolean}
196   * @private
197   */
198  
199  SendStream.prototype.hasTrailingSlash = function hasTrailingSlash () {
200    return this.path[this.path.length - 1] === '/'
201  }
202  
203  /**
204   * Check if this is a conditional GET request.
205   *
206   * @return {Boolean}
207   * @api private
208   */
209  
210  SendStream.prototype.isConditionalGET = function isConditionalGET () {
211    return this.req.headers['if-match'] ||
212      this.req.headers['if-unmodified-since'] ||
213      this.req.headers['if-none-match'] ||
214      this.req.headers['if-modified-since']
215  }
216  
217  /**
218   * Check if the request preconditions failed.
219   *
220   * @return {boolean}
221   * @private
222   */
223  
224  SendStream.prototype.isPreconditionFailure = function isPreconditionFailure () {
225    var req = this.req
226    var res = this.res
227  
228    // if-match
229    var match = req.headers['if-match']
230    if (match) {
231      var etag = res.getHeader('ETag')
232      return !etag || (match !== '*' && parseTokenList(match).every(function (match) {
233        return match !== etag && match !== 'W/' + etag && 'W/' + match !== etag
234      }))
235    }
236  
237    // if-unmodified-since
238    var unmodifiedSince = parseHttpDate(req.headers['if-unmodified-since'])
239    if (!isNaN(unmodifiedSince)) {
240      var lastModified = parseHttpDate(res.getHeader('Last-Modified'))
241      return isNaN(lastModified) || lastModified > unmodifiedSince
242    }
243  
244    return false
245  }
246  
247  /**
248   * Strip various content header fields for a change in entity.
249   *
250   * @private
251   */
252  
253  SendStream.prototype.removeContentHeaderFields = function removeContentHeaderFields () {
254    var res = this.res
255  
256    res.removeHeader('Content-Encoding')
257    res.removeHeader('Content-Language')
258    res.removeHeader('Content-Length')
259    res.removeHeader('Content-Range')
260    res.removeHeader('Content-Type')
261  }
262  
263  /**
264   * Respond with 304 not modified.
265   *
266   * @api private
267   */
268  
269  SendStream.prototype.notModified = function notModified () {
270    var res = this.res
271    debug('not modified')
272    this.removeContentHeaderFields()
273    res.statusCode = 304
274    res.end()
275  }
276  
277  /**
278   * Raise error that headers already sent.
279   *
280   * @api private
281   */
282  
283  SendStream.prototype.headersAlreadySent = function headersAlreadySent () {
284    var err = new Error('Can\'t set headers after they are sent.')
285    debug('headers already sent')
286    this.error(500, err)
287  }
288  
289  /**
290   * Check if the request is cacheable, aka
291   * responded with 2xx or 304 (see RFC 2616 section 14.2{5,6}).
292   *
293   * @return {Boolean}
294   * @api private
295   */
296  
297  SendStream.prototype.isCachable = function isCachable () {
298    var statusCode = this.res.statusCode
299    return (statusCode >= 200 && statusCode < 300) ||
300      statusCode === 304
301  }
302  
303  /**
304   * Handle stat() error.
305   *
306   * @param {Error} error
307   * @private
308   */
309  
310  SendStream.prototype.onStatError = function onStatError (error) {
311    switch (error.code) {
312      case 'ENAMETOOLONG':
313      case 'ENOENT':
314      case 'ENOTDIR':
315        this.error(404, error)
316        break
317      default:
318        this.error(500, error)
319        break
320    }
321  }
322  
323  /**
324   * Check if the cache is fresh.
325   *
326   * @return {Boolean}
327   * @api private
328   */
329  
330  SendStream.prototype.isFresh = function isFresh () {
331    return fresh(this.req.headers, {
332      etag: this.res.getHeader('ETag'),
333      'last-modified': this.res.getHeader('Last-Modified')
334    })
335  }
336  
337  /**
338   * Check if the range is fresh.
339   *
340   * @return {Boolean}
341   * @api private
342   */
343  
344  SendStream.prototype.isRangeFresh = function isRangeFresh () {
345    var ifRange = this.req.headers['if-range']
346  
347    if (!ifRange) {
348      return true
349    }
350  
351    // if-range as etag
352    if (ifRange.indexOf('"') !== -1) {
353      var etag = this.res.getHeader('ETag')
354      return Boolean(etag && ifRange.indexOf(etag) !== -1)
355    }
356  
357    // if-range as modified date
358    var lastModified = this.res.getHeader('Last-Modified')
359    return parseHttpDate(lastModified) <= parseHttpDate(ifRange)
360  }
361  
362  /**
363   * Redirect to path.
364   *
365   * @param {string} path
366   * @private
367   */
368  
369  SendStream.prototype.redirect = function redirect (path) {
370    var res = this.res
371  
372    if (hasListeners(this, 'directory')) {
373      this.emit('directory', res, path)
374      return
375    }
376  
377    if (this.hasTrailingSlash()) {
378      this.error(403)
379      return
380    }
381  
382    var loc = encodeUrl(collapseLeadingSlashes(this.path + '/'))
383    var doc = createHtmlDocument('Redirecting', 'Redirecting to ' + escapeHtml(loc))
384  
385    // redirect
386    res.statusCode = 301
387    res.setHeader('Content-Type', 'text/html; charset=UTF-8')
388    res.setHeader('Content-Length', Buffer.byteLength(doc))
389    res.setHeader('Content-Security-Policy', "default-src 'none'")
390    res.setHeader('X-Content-Type-Options', 'nosniff')
391    res.setHeader('Location', loc)
392    res.end(doc)
393  }
394  
395  /**
396   * Pipe to `res.
397   *
398   * @param {Stream} res
399   * @return {Stream} res
400   * @api public
401   */
402  
403  SendStream.prototype.pipe = function pipe (res) {
404    // root path
405    var root = this._root
406  
407    // references
408    this.res = res
409  
410    // decode the path
411    var path = decode(this.path)
412    if (path === -1) {
413      this.error(400)
414      return res
415    }
416  
417    // null byte(s)
418    if (~path.indexOf('\0')) {
419      this.error(400)
420      return res
421    }
422  
423    var parts
424    if (root !== null) {
425      // normalize
426      if (path) {
427        path = normalize('.' + sep + path)
428      }
429  
430      // malicious path
431      if (UP_PATH_REGEXP.test(path)) {
432        debug('malicious path "%s"', path)
433        this.error(403)
434        return res
435      }
436  
437      // explode path parts
438      parts = path.split(sep)
439  
440      // join / normalize from optional root dir
441      path = normalize(join(root, path))
442    } else {
443      // ".." is malicious without "root"
444      if (UP_PATH_REGEXP.test(path)) {
445        debug('malicious path "%s"', path)
446        this.error(403)
447        return res
448      }
449  
450      // explode path parts
451      parts = normalize(path).split(sep)
452  
453      // resolve the path
454      path = resolve(path)
455    }
456  
457    // dotfile handling
458    if (containsDotFile(parts)) {
459      debug('%s dotfile "%s"', this._dotfiles, path)
460      switch (this._dotfiles) {
461        case 'allow':
462          break
463        case 'deny':
464          this.error(403)
465          return res
466        case 'ignore':
467        default:
468          this.error(404)
469          return res
470      }
471    }
472  
473    // index file support
474    if (this._index.length && this.hasTrailingSlash()) {
475      this.sendIndex(path)
476      return res
477    }
478  
479    this.sendFile(path)
480    return res
481  }
482  
483  /**
484   * Transfer `path`.
485   *
486   * @param {String} path
487   * @api public
488   */
489  
490  SendStream.prototype.send = function send (path, stat) {
491    var len = stat.size
492    var options = this.options
493    var opts = {}
494    var res = this.res
495    var req = this.req
496    var ranges = req.headers.range
497    var offset = options.start || 0
498  
499    if (res.headersSent) {
500      // impossible to send now
501      this.headersAlreadySent()
502      return
503    }
504  
505    debug('pipe "%s"', path)
506  
507    // set header fields
508    this.setHeader(path, stat)
509  
510    // set content-type
511    this.type(path)
512  
513    // conditional GET support
514    if (this.isConditionalGET()) {
515      if (this.isPreconditionFailure()) {
516        this.error(412)
517        return
518      }
519  
520      if (this.isCachable() && this.isFresh()) {
521        this.notModified()
522        return
523      }
524    }
525  
526    // adjust len to start/end options
527    len = Math.max(0, len - offset)
528    if (options.end !== undefined) {
529      var bytes = options.end - offset + 1
530      if (len > bytes) len = bytes
531    }
532  
533    // Range support
534    if (this._acceptRanges && BYTES_RANGE_REGEXP.test(ranges)) {
535      // parse
536      ranges = parseRange(len, ranges, {
537        combine: true
538      })
539  
540      // If-Range support
541      if (!this.isRangeFresh()) {
542        debug('range stale')
543        ranges = -2
544      }
545  
546      // unsatisfiable
547      if (ranges === -1) {
548        debug('range unsatisfiable')
549  
550        // Content-Range
551        res.setHeader('Content-Range', contentRange('bytes', len))
552  
553        // 416 Requested Range Not Satisfiable
554        return this.error(416, {
555          headers: { 'Content-Range': res.getHeader('Content-Range') }
556        })
557      }
558  
559      // valid (syntactically invalid/multiple ranges are treated as a regular response)
560      if (ranges !== -2 && ranges.length === 1) {
561        debug('range %j', ranges)
562  
563        // Content-Range
564        res.statusCode = 206
565        res.setHeader('Content-Range', contentRange('bytes', len, ranges[0]))
566  
567        // adjust for requested range
568        offset += ranges[0].start
569        len = ranges[0].end - ranges[0].start + 1
570      }
571    }
572  
573    // clone options
574    for (var prop in options) {
575      opts[prop] = options[prop]
576    }
577  
578    // set read options
579    opts.start = offset
580    opts.end = Math.max(offset, offset + len - 1)
581  
582    // content-length
583    res.setHeader('Content-Length', len)
584  
585    // HEAD support
586    if (req.method === 'HEAD') {
587      res.end()
588      return
589    }
590  
591    this.stream(path, opts)
592  }
593  
594  /**
595   * Transfer file for `path`.
596   *
597   * @param {String} path
598   * @api private
599   */
600  SendStream.prototype.sendFile = function sendFile (path) {
601    var i = 0
602    var self = this
603  
604    debug('stat "%s"', path)
605    fs.stat(path, function onstat (err, stat) {
606      var pathEndsWithSep = path[path.length - 1] === sep
607      if (err && err.code === 'ENOENT' && !extname(path) && !pathEndsWithSep) {
608        // not found, check extensions
609        return next(err)
610      }
611      if (err) return self.onStatError(err)
612      if (stat.isDirectory()) return self.redirect(path)
613      if (pathEndsWithSep) return self.error(404)
614      self.emit('file', path, stat)
615      self.send(path, stat)
616    })
617  
618    function next (err) {
619      if (self._extensions.length <= i) {
620        return err
621          ? self.onStatError(err)
622          : self.error(404)
623      }
624  
625      var p = path + '.' + self._extensions[i++]
626  
627      debug('stat "%s"', p)
628      fs.stat(p, function (err, stat) {
629        if (err) return next(err)
630        if (stat.isDirectory()) return next()
631        self.emit('file', p, stat)
632        self.send(p, stat)
633      })
634    }
635  }
636  
637  /**
638   * Transfer index for `path`.
639   *
640   * @param {String} path
641   * @api private
642   */
643  SendStream.prototype.sendIndex = function sendIndex (path) {
644    var i = -1
645    var self = this
646  
647    function next (err) {
648      if (++i >= self._index.length) {
649        if (err) return self.onStatError(err)
650        return self.error(404)
651      }
652  
653      var p = join(path, self._index[i])
654  
655      debug('stat "%s"', p)
656      fs.stat(p, function (err, stat) {
657        if (err) return next(err)
658        if (stat.isDirectory()) return next()
659        self.emit('file', p, stat)
660        self.send(p, stat)
661      })
662    }
663  
664    next()
665  }
666  
667  /**
668   * Stream `path` to the response.
669   *
670   * @param {String} path
671   * @param {Object} options
672   * @api private
673   */
674  
675  SendStream.prototype.stream = function stream (path, options) {
676    var self = this
677    var res = this.res
678  
679    // pipe
680    var stream = fs.createReadStream(path, options)
681    this.emit('stream', stream)
682    stream.pipe(res)
683  
684    // cleanup
685    function cleanup () {
686      stream.destroy()
687    }
688  
689    // response finished, cleanup
690    onFinished(res, cleanup)
691  
692    // error handling
693    stream.on('error', function onerror (err) {
694      // clean up stream early
695      cleanup()
696  
697      // error
698      self.onStatError(err)
699    })
700  
701    // end
702    stream.on('end', function onend () {
703      self.emit('end')
704    })
705  }
706  
707  /**
708   * Set content-type based on `path`
709   * if it hasn't been explicitly set.
710   *
711   * @param {String} path
712   * @api private
713   */
714  
715  SendStream.prototype.type = function type (path) {
716    var res = this.res
717  
718    if (res.getHeader('Content-Type')) return
719  
720    var ext = extname(path)
721    var type = mime.contentType(ext) || 'application/octet-stream'
722  
723    debug('content-type %s', type)
724    res.setHeader('Content-Type', type)
725  }
726  
727  /**
728   * Set response header fields, most
729   * fields may be pre-defined.
730   *
731   * @param {String} path
732   * @param {Object} stat
733   * @api private
734   */
735  
736  SendStream.prototype.setHeader = function setHeader (path, stat) {
737    var res = this.res
738  
739    this.emit('headers', res, path, stat)
740  
741    if (this._acceptRanges && !res.getHeader('Accept-Ranges')) {
742      debug('accept ranges')
743      res.setHeader('Accept-Ranges', 'bytes')
744    }
745  
746    if (this._cacheControl && !res.getHeader('Cache-Control')) {
747      var cacheControl = 'public, max-age=' + Math.floor(this._maxage / 1000)
748  
749      if (this._immutable) {
750        cacheControl += ', immutable'
751      }
752  
753      debug('cache-control %s', cacheControl)
754      res.setHeader('Cache-Control', cacheControl)
755    }
756  
757    if (this._lastModified && !res.getHeader('Last-Modified')) {
758      var modified = stat.mtime.toUTCString()
759      debug('modified %s', modified)
760      res.setHeader('Last-Modified', modified)
761    }
762  
763    if (this._etag && !res.getHeader('ETag')) {
764      var val = etag(stat)
765      debug('etag %s', val)
766      res.setHeader('ETag', val)
767    }
768  }
769  
770  /**
771   * Clear all headers from a response.
772   *
773   * @param {object} res
774   * @private
775   */
776  
777  function clearHeaders (res) {
778    for (const header of res.getHeaderNames()) {
779      res.removeHeader(header)
780    }
781  }
782  
783  /**
784   * Collapse all leading slashes into a single slash
785   *
786   * @param {string} str
787   * @private
788   */
789  function collapseLeadingSlashes (str) {
790    for (var i = 0; i < str.length; i++) {
791      if (str[i] !== '/') {
792        break
793      }
794    }
795  
796    return i > 1
797      ? '/' + str.substr(i)
798      : str
799  }
800  
801  /**
802   * Determine if path parts contain a dotfile.
803   *
804   * @api private
805   */
806  
807  function containsDotFile (parts) {
808    for (var i = 0; i < parts.length; i++) {
809      var part = parts[i]
810      if (part.length > 1 && part[0] === '.') {
811        return true
812      }
813    }
814  
815    return false
816  }
817  
818  /**
819   * Create a Content-Range header.
820   *
821   * @param {string} type
822   * @param {number} size
823   * @param {array} [range]
824   */
825  
826  function contentRange (type, size, range) {
827    return type + ' ' + (range ? range.start + '-' + range.end : '*') + '/' + size
828  }
829  
830  /**
831   * Create a minimal HTML document.
832   *
833   * @param {string} title
834   * @param {string} body
835   * @private
836   */
837  
838  function createHtmlDocument (title, body) {
839    return '<!DOCTYPE html>\n' +
840      '<html lang="en">\n' +
841      '<head>\n' +
842      '<meta charset="utf-8">\n' +
843      '<title>' + title + '</title>\n' +
844      '</head>\n' +
845      '<body>\n' +
846      '<pre>' + body + '</pre>\n' +
847      '</body>\n' +
848      '</html>\n'
849  }
850  
851  /**
852   * Create a HttpError object from simple arguments.
853   *
854   * @param {number} status
855   * @param {Error|object} err
856   * @private
857   */
858  
859  function createHttpError (status, err) {
860    if (!err) {
861      return createError(status)
862    }
863  
864    return err instanceof Error
865      ? createError(status, err, { expose: false })
866      : createError(status, err)
867  }
868  
869  /**
870   * decodeURIComponent.
871   *
872   * Allows V8 to only deoptimize this fn instead of all
873   * of send().
874   *
875   * @param {String} path
876   * @api private
877   */
878  
879  function decode (path) {
880    try {
881      return decodeURIComponent(path)
882    } catch (err) {
883      return -1
884    }
885  }
886  
887  /**
888   * Determine if emitter has listeners of a given type.
889   *
890   * The way to do this check is done three different ways in Node.js >= 0.10
891   * so this consolidates them into a minimal set using instance methods.
892   *
893   * @param {EventEmitter} emitter
894   * @param {string} type
895   * @returns {boolean}
896   * @private
897   */
898  
899  function hasListeners (emitter, type) {
900    var count = typeof emitter.listenerCount !== 'function'
901      ? emitter.listeners(type).length
902      : emitter.listenerCount(type)
903  
904    return count > 0
905  }
906  
907  /**
908   * Normalize the index option into an array.
909   *
910   * @param {boolean|string|array} val
911   * @param {string} name
912   * @private
913   */
914  
915  function normalizeList (val, name) {
916    var list = [].concat(val || [])
917  
918    for (var i = 0; i < list.length; i++) {
919      if (typeof list[i] !== 'string') {
920        throw new TypeError(name + ' must be array of strings or false')
921      }
922    }
923  
924    return list
925  }
926  
927  /**
928   * Parse an HTTP Date into a number.
929   *
930   * @param {string} date
931   * @private
932   */
933  
934  function parseHttpDate (date) {
935    var timestamp = date && Date.parse(date)
936  
937    return typeof timestamp === 'number'
938      ? timestamp
939      : NaN
940  }
941  
942  /**
943   * Parse a HTTP token list.
944   *
945   * @param {string} str
946   * @private
947   */
948  
949  function parseTokenList (str) {
950    var end = 0
951    var list = []
952    var start = 0
953  
954    // gather tokens
955    for (var i = 0, len = str.length; i < len; i++) {
956      switch (str.charCodeAt(i)) {
957        case 0x20: /*   */
958          if (start === end) {
959            start = end = i + 1
960          }
961          break
962        case 0x2c: /* , */
963          if (start !== end) {
964            list.push(str.substring(start, end))
965          }
966          start = end = i + 1
967          break
968        default:
969          end = i + 1
970          break
971      }
972    }
973  
974    // final token
975    if (start !== end) {
976      list.push(str.substring(start, end))
977    }
978  
979    return list
980  }
981  
982  /**
983   * Set an object of headers on a response.
984   *
985   * @param {object} res
986   * @param {object} headers
987   * @private
988   */
989  
990  function setHeaders (res, headers) {
991    var keys = Object.keys(headers)
992  
993    for (var i = 0; i < keys.length; i++) {
994      var key = keys[i]
995      res.setHeader(key, headers[key])
996    }
997  }