index.js (6059B)
1 /*! 2 * raw-body 3 * Copyright(c) 2013-2014 Jonathan Ong 4 * Copyright(c) 2014-2015 Douglas Christopher Wilson 5 * MIT Licensed 6 */ 7 8 'use strict' 9 10 /** 11 * Module dependencies. 12 * @private 13 */ 14 15 var bytes = require('bytes') 16 var createError = require('http-errors') 17 var iconv = require('iconv-lite') 18 var unpipe = require('unpipe') 19 20 /** 21 * Module exports. 22 * @public 23 */ 24 25 module.exports = getRawBody 26 27 /** 28 * Module variables. 29 * @private 30 */ 31 32 var ICONV_ENCODING_MESSAGE_REGEXP = /^Encoding not recognized: / 33 34 /** 35 * Get the decoder for a given encoding. 36 * 37 * @param {string} encoding 38 * @private 39 */ 40 41 function getDecoder (encoding) { 42 if (!encoding) return null 43 44 try { 45 return iconv.getDecoder(encoding) 46 } catch (e) { 47 // error getting decoder 48 if (!ICONV_ENCODING_MESSAGE_REGEXP.test(e.message)) throw e 49 50 // the encoding was not found 51 throw createError(415, 'specified encoding unsupported', { 52 encoding: encoding, 53 type: 'encoding.unsupported' 54 }) 55 } 56 } 57 58 /** 59 * Get the raw body of a stream (typically HTTP). 60 * 61 * @param {object} stream 62 * @param {object|string|function} [options] 63 * @param {function} [callback] 64 * @public 65 */ 66 67 function getRawBody (stream, options, callback) { 68 var done = callback 69 var opts = options || {} 70 71 if (options === true || typeof options === 'string') { 72 // short cut for encoding 73 opts = { 74 encoding: options 75 } 76 } 77 78 if (typeof options === 'function') { 79 done = options 80 opts = {} 81 } 82 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') 86 } 87 88 // require the callback without promises 89 if (!done && !global.Promise) { 90 throw new TypeError('argument callback is required') 91 } 92 93 // get encoding 94 var encoding = opts.encoding !== true 95 ? opts.encoding 96 : 'utf-8' 97 98 // convert the limit to an integer 99 var limit = bytes.parse(opts.limit) 100 101 // convert the expected length to an integer 102 var length = opts.length != null && !isNaN(opts.length) 103 ? parseInt(opts.length, 10) 104 : null 105 106 if (done) { 107 // classic callback style 108 return readStream(stream, encoding, length, limit, done) 109 } 110 111 return new Promise(function executor (resolve, reject) { 112 readStream(stream, encoding, length, limit, function onRead (err, buf) { 113 if (err) return reject(err) 114 resolve(buf) 115 }) 116 }) 117 } 118 119 /** 120 * Halt a stream. 121 * 122 * @param {Object} stream 123 * @private 124 */ 125 126 function halt (stream) { 127 // unpipe everything from the stream 128 unpipe(stream) 129 130 // pause stream 131 if (typeof stream.pause === 'function') { 132 stream.pause() 133 } 134 } 135 136 /** 137 * Read the data from the stream. 138 * 139 * @param {object} stream 140 * @param {string} encoding 141 * @param {number} length 142 * @param {number} limit 143 * @param {function} callback 144 * @public 145 */ 146 147 function readStream (stream, encoding, length, limit, callback) { 148 var complete = false 149 var sync = true 150 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', { 156 expected: length, 157 length: length, 158 limit: limit, 159 type: 'entity.too.large' 160 })) 161 } 162 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))) { 170 // developer error 171 return done(createError(500, 'stream encoding should not be set', { 172 type: 'stream.encoding.set' 173 })) 174 } 175 176 var received = 0 177 var decoder 178 179 try { 180 decoder = getDecoder(encoding) 181 } catch (err) { 182 return done(err) 183 } 184 185 var buffer = decoder 186 ? '' 187 : [] 188 189 // attach listeners 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) 195 196 // mark sync section complete 197 sync = false 198 199 function done () { 200 var args = new Array(arguments.length) 201 202 // copy arguments 203 for (var i = 0; i < args.length; i++) { 204 args[i] = arguments[i] 205 } 206 207 // mark complete 208 complete = true 209 210 if (sync) { 211 process.nextTick(invokeCallback) 212 } else { 213 invokeCallback() 214 } 215 216 function invokeCallback () { 217 cleanup() 218 219 if (args[0]) { 220 // halt the stream on error 221 halt(stream) 222 } 223 224 callback.apply(null, args) 225 } 226 } 227 228 function onAborted () { 229 if (complete) return 230 231 done(createError(400, 'request aborted', { 232 code: 'ECONNABORTED', 233 expected: length, 234 length: length, 235 received: received, 236 type: 'request.aborted' 237 })) 238 } 239 240 function onData (chunk) { 241 if (complete) return 242 243 received += chunk.length 244 245 if (limit !== null && received > limit) { 246 done(createError(413, 'request entity too large', { 247 limit: limit, 248 received: received, 249 type: 'entity.too.large' 250 })) 251 } else if (decoder) { 252 buffer += decoder.write(chunk) 253 } else { 254 buffer.push(chunk) 255 } 256 } 257 258 function onEnd (err) { 259 if (complete) return 260 if (err) return done(err) 261 262 if (length !== null && received !== length) { 263 done(createError(400, 'request size did not match content length', { 264 expected: length, 265 length: length, 266 received: received, 267 type: 'request.size.invalid' 268 })) 269 } else { 270 var string = decoder 271 ? buffer + (decoder.end() || '') 272 : Buffer.concat(buffer) 273 done(null, string) 274 } 275 } 276 277 function cleanup () { 278 buffer = null 279 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) 285 } 286 }