3 * Copyright(c) 2013-2014 Jonathan Ong
4 * Copyright(c) 2014-2015 Douglas Christopher Wilson
11 * Module dependencies.
15 var bytes = require('bytes')
16 var createError = require('http-errors')
17 var iconv = require('iconv-lite')
18 var unpipe = require('unpipe')
25 module.exports = getRawBody
32 var ICONV_ENCODING_MESSAGE_REGEXP = /^Encoding not recognized: /
35 * Get the decoder for a given encoding.
37 * @param {string} encoding
41 function getDecoder (encoding) {
42 if (!encoding) return null
45 return iconv.getDecoder(encoding)
47 // error getting decoder
48 if (!ICONV_ENCODING_MESSAGE_REGEXP.test(e.message)) throw e
50 // the encoding was not found
51 throw createError(415, 'specified encoding unsupported', {
53 type: 'encoding.unsupported'
59 * Get the raw body of a stream (typically HTTP).
61 * @param {object} stream
62 * @param {object|string|function} [options]
63 * @param {function} [callback]
67 function getRawBody (stream, options, callback) {
69 var opts = options || {}
71 if (options === true || typeof options === 'string') {
72 // short cut for encoding
78 if (typeof options === 'function') {
83 // validate callback is a function, if provided
84 if (done !== undefined && typeof done !== 'function') {
85 throw new TypeError('argument callback must be a function')
88 // require the callback without promises
89 if (!done && !global.Promise) {
90 throw new TypeError('argument callback is required')
94 var encoding = opts.encoding !== true
98 // convert the limit to an integer
99 var limit = bytes.parse(opts.limit)
101 // convert the expected length to an integer
102 var length = opts.length != null && !isNaN(opts.length)
103 ? parseInt(opts.length, 10)
107 // classic callback style
108 return readStream(stream, encoding, length, limit, done)
111 return new Promise(function executor (resolve, reject) {
112 readStream(stream, encoding, length, limit, function onRead (err, buf) {
113 if (err) return reject(err)
122 * @param {Object} stream
126 function halt (stream) {
127 // unpipe everything from the stream
131 if (typeof stream.pause === 'function') {
137 * Read the data from the stream.
139 * @param {object} stream
140 * @param {string} encoding
141 * @param {number} length
142 * @param {number} limit
143 * @param {function} callback
147 function readStream (stream, encoding, length, limit, callback) {
151 // check the length and limit options.
152 // note: we intentionally leave the stream paused,
153 // so users should handle the stream themselves.
154 if (limit !== null && length !== null && length > limit) {
155 return done(createError(413, 'request entity too large', {
159 type: 'entity.too.large'
163 // streams1: assert request encoding is buffer.
164 // streams2+: assert the stream encoding is buffer.
165 // stream._decoder: streams1
166 // state.encoding: streams2
167 // state.decoder: streams2, specifically < 0.10.6
168 var state = stream._readableState
169 if (stream._decoder || (state && (state.encoding || state.decoder))) {
171 return done(createError(500, 'stream encoding should not be set', {
172 type: 'stream.encoding.set'
180 decoder = getDecoder(encoding)
190 stream.on('aborted', onAborted)
191 stream.on('close', cleanup)
192 stream.on('data', onData)
193 stream.on('end', onEnd)
194 stream.on('error', onEnd)
196 // mark sync section complete
200 var args = new Array(arguments.length)
203 for (var i = 0; i < args.length; i++) {
204 args[i] = arguments[i]
211 process.nextTick(invokeCallback)
216 function invokeCallback () {
220 // halt the stream on error
224 callback.apply(null, args)
228 function onAborted () {
231 done(createError(400, 'request aborted', {
232 code: 'ECONNABORTED',
236 type: 'request.aborted'
240 function onData (chunk) {
243 received += chunk.length
245 if (limit !== null && received > limit) {
246 done(createError(413, 'request entity too large', {
249 type: 'entity.too.large'
251 } else if (decoder) {
252 buffer += decoder.write(chunk)
258 function onEnd (err) {
260 if (err) return done(err)
262 if (length !== null && received !== length) {
263 done(createError(400, 'request size did not match content length', {
267 type: 'request.size.invalid'
271 ? buffer + (decoder.end() || '')
272 : Buffer.concat(buffer)
277 function cleanup () {
280 stream.removeListener('aborted', onAborted)
281 stream.removeListener('data', onData)
282 stream.removeListener('end', onEnd)
283 stream.removeListener('error', onEnd)
284 stream.removeListener('close', cleanup)