Spaces:
Running
Running
/*! | |
* raw-body | |
* Copyright(c) 2013-2014 Jonathan Ong | |
* Copyright(c) 2014-2022 Douglas Christopher Wilson | |
* MIT Licensed | |
*/ | |
/** | |
* 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 || {} | |
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) | |
} | |