Skip to content
Snippets Groups Projects
index.js 7 KiB
Newer Older
  • Learn to ignore specific revisions
  • Jay's avatar
    Jay committed
    /*!
     * raw-body
     * Copyright(c) 2013-2014 Jonathan Ong
     * Copyright(c) 2014-2022 Douglas Christopher Wilson
     * MIT Licensed
     */
    
    'use strict'
    
    /**
     * Module dependencies.
     * @private
     */
    
    var asyncHooks = tryRequireAsyncHooks()
    var bytes = require('bytes')
    var createError = require('http-errors')
    var iconv = require('iconv-lite')
    var unpipe = require('unpipe')
    
    /**
     * Module exports.
     * @public
     */
    
    module.exports = getRawBody
    
    /**
     * Module variables.
     * @private
     */
    
    var ICONV_ENCODING_MESSAGE_REGEXP = /^Encoding not recognized: /
    
    /**
     * Get the decoder for a given encoding.
     *
     * @param {string} encoding
     * @private
     */
    
    function getDecoder (encoding) {
      if (!encoding) return null
    
      try {
        return iconv.getDecoder(encoding)
      } catch (e) {
        // error getting decoder
        if (!ICONV_ENCODING_MESSAGE_REGEXP.test(e.message)) throw e
    
        // the encoding was not found
        throw createError(415, 'specified encoding unsupported', {
          encoding: encoding,
          type: 'encoding.unsupported'
        })
      }
    }
    
    /**
     * Get the raw body of a stream (typically HTTP).
     *
     * @param {object} stream
     * @param {object|string|function} [options]
     * @param {function} [callback]
     * @public
     */
    
    function getRawBody (stream, options, callback) {
      var done = callback
      var opts = options || {}
    
      // light validation
      if (stream === undefined) {
        throw new TypeError('argument stream is required')
      } else if (typeof stream !== 'object' || stream === null || typeof stream.on !== 'function') {
        throw new TypeError('argument stream must be a stream')
      }
    
      if (options === true || typeof options === 'string') {
        // short cut for encoding
        opts = {
          encoding: options
        }
      }
    
      if (typeof options === 'function') {
        done = options
        opts = {}
      }
    
      // validate callback is a function, if provided
      if (done !== undefined && typeof done !== 'function') {
        throw new TypeError('argument callback must be a function')
      }
    
      // require the callback without promises
      if (!done && !global.Promise) {
        throw new TypeError('argument callback is required')
      }
    
      // get encoding
      var encoding = opts.encoding !== true
        ? opts.encoding
        : 'utf-8'
    
      // convert the limit to an integer
      var limit = bytes.parse(opts.limit)
    
      // convert the expected length to an integer
      var length = opts.length != null && !isNaN(opts.length)
        ? parseInt(opts.length, 10)
        : null
    
      if (done) {
        // classic callback style
        return readStream(stream, encoding, length, limit, wrap(done))
      }
    
      return new Promise(function executor (resolve, reject) {
        readStream(stream, encoding, length, limit, function onRead (err, buf) {
          if (err) return reject(err)
          resolve(buf)
        })
      })
    }
    
    /**
     * Halt a stream.
     *
     * @param {Object} stream
     * @private
     */
    
    function halt (stream) {
      // unpipe everything from the stream
      unpipe(stream)
    
      // pause stream
      if (typeof stream.pause === 'function') {
        stream.pause()
      }
    }
    
    /**
     * Read the data from the stream.
     *
     * @param {object} stream
     * @param {string} encoding
     * @param {number} length
     * @param {number} limit
     * @param {function} callback
     * @public
     */
    
    function readStream (stream, encoding, length, limit, callback) {
      var complete = false
      var sync = true
    
      // check the length and limit options.
      // note: we intentionally leave the stream paused,
      // so users should handle the stream themselves.
      if (limit !== null && length !== null && length > limit) {
        return done(createError(413, 'request entity too large', {
          expected: length,
          length: length,
          limit: limit,
          type: 'entity.too.large'
        }))
      }
    
      // streams1: assert request encoding is buffer.
      // streams2+: assert the stream encoding is buffer.
      //   stream._decoder: streams1
      //   state.encoding: streams2
      //   state.decoder: streams2, specifically < 0.10.6
      var state = stream._readableState
      if (stream._decoder || (state && (state.encoding || state.decoder))) {
        // developer error
        return done(createError(500, 'stream encoding should not be set', {
          type: 'stream.encoding.set'
        }))
      }
    
      if (typeof stream.readable !== 'undefined' && !stream.readable) {
        return done(createError(500, 'stream is not readable', {
          type: 'stream.not.readable'
        }))
      }
    
      var received = 0
      var decoder
    
      try {
        decoder = getDecoder(encoding)
      } catch (err) {
        return done(err)
      }
    
      var buffer = decoder
        ? ''
        : []
    
      // attach listeners
      stream.on('aborted', onAborted)
      stream.on('close', cleanup)
      stream.on('data', onData)
      stream.on('end', onEnd)
      stream.on('error', onEnd)
    
      // mark sync section complete
      sync = false
    
      function done () {
        var args = new Array(arguments.length)
    
        // copy arguments
        for (var i = 0; i < args.length; i++) {
          args[i] = arguments[i]
        }
    
        // mark complete
        complete = true
    
        if (sync) {
          process.nextTick(invokeCallback)
        } else {
          invokeCallback()
        }
    
        function invokeCallback () {
          cleanup()
    
          if (args[0]) {
            // halt the stream on error
            halt(stream)
          }
    
          callback.apply(null, args)
        }
      }
    
      function onAborted () {
        if (complete) return
    
        done(createError(400, 'request aborted', {
          code: 'ECONNABORTED',
          expected: length,
          length: length,
          received: received,
          type: 'request.aborted'
        }))
      }
    
      function onData (chunk) {
        if (complete) return
    
        received += chunk.length
    
        if (limit !== null && received > limit) {
          done(createError(413, 'request entity too large', {
            limit: limit,
            received: received,
            type: 'entity.too.large'
          }))
        } else if (decoder) {
          buffer += decoder.write(chunk)
        } else {
          buffer.push(chunk)
        }
      }
    
      function onEnd (err) {
        if (complete) return
        if (err) return done(err)
    
        if (length !== null && received !== length) {
          done(createError(400, 'request size did not match content length', {
            expected: length,
            length: length,
            received: received,
            type: 'request.size.invalid'
          }))
        } else {
          var string = decoder
            ? buffer + (decoder.end() || '')
            : Buffer.concat(buffer)
          done(null, string)
        }
      }
    
      function cleanup () {
        buffer = null
    
        stream.removeListener('aborted', onAborted)
        stream.removeListener('data', onData)
        stream.removeListener('end', onEnd)
        stream.removeListener('error', onEnd)
        stream.removeListener('close', cleanup)
      }
    }
    
    /**
     * Try to require async_hooks
     * @private
     */
    
    function tryRequireAsyncHooks () {
      try {
        return require('async_hooks')
      } catch (e) {
        return {}
      }
    }
    
    /**
     * Wrap function with async resource, if possible.
     * AsyncResource.bind static method backported.
     * @private
     */
    
    function wrap (fn) {
      var res
    
      // create anonymous resource
      if (asyncHooks.AsyncResource) {
        res = new asyncHooks.AsyncResource(fn.name || 'bound-anonymous-fn')
      }
    
      // incompatible node.js
      if (!res || !res.runInAsyncScope) {
        return fn
      }
    
      // return bound function
      return res.runInAsyncScope.bind(res, fn, null)
    }