/** * body.js * * Body interface provides common methods for Request and Response */ var convert = require('encoding').convert; var bodyStream = require('is-stream'); var PassThrough = require('stream').PassThrough; var FetchError = require('./fetch-error'); module.exports = Body; /** * Body class * * @param Stream body Readable stream * @param Object opts Response options * @return Void */ function Body(body, opts) { opts = opts || {}; this.body = body; this.bodyUsed = false; this.size = opts.size || 0; this.timeout = opts.timeout || 0; this._raw = []; this._abort = false; } /** * Decode response as json * * @return Promise */ Body.prototype.json = function() { var self = this; return this._decode().then(function(buffer) { try { return JSON.parse(buffer.toString()); } catch (err) { return Body.Promise.reject(new FetchError('invalid json response body at ' + self.url + ' reason: ' + err.message, 'invalid-json')); } }); }; /** * Decode response as text * * @return Promise */ Body.prototype.text = function() { return this._decode().then(function(buffer) { return buffer.toString(); }); }; /** * Decode response as buffer (non-spec api) * * @return Promise */ Body.prototype.buffer = function() { return this._decode(); }; /** * Decode buffers into utf-8 string * * @return Promise */ Body.prototype._decode = function() { var self = this; if (this.bodyUsed) { return Body.Promise.reject(new Error('body used already for: ' + this.url)); } this.bodyUsed = true; this._bytes = 0; this._abort = false; this._raw = []; return new Body.Promise(function(resolve, reject) { var resTimeout; // body is string if (typeof self.body === 'string') { self._bytes = self.body.length; self._raw = [new Buffer(self.body)]; return resolve(self._convert()); } // body is buffer if (self.body instanceof Buffer) { self._bytes = self.body.length; self._raw = [self.body]; return resolve(self._convert()); } // allow timeout on slow response body if (self.timeout) { resTimeout = setTimeout(function() { self._abort = true; reject(new FetchError('response timeout at ' + self.url + ' over limit: ' + self.timeout, 'body-timeout')); }, self.timeout); } // handle stream error, such as incorrect content-encoding self.body.on('error', function(err) { reject(new FetchError('invalid response body at: ' + self.url + ' reason: ' + err.message, 'system', err)); }); // body is stream self.body.on('data', function(chunk) { if (self._abort || chunk === null) { return; } if (self.size && self._bytes + chunk.length > self.size) { self._abort = true; reject(new FetchError('content size at ' + self.url + ' over limit: ' + self.size, 'max-size')); return; } self._bytes += chunk.length; self._raw.push(chunk); }); self.body.on('end', function() { if (self._abort) { return; } clearTimeout(resTimeout); resolve(self._convert()); }); }); }; /** * Detect buffer encoding and convert to target encoding * ref: http://www.w3.org/TR/2011/WD-html5-20110113/parsing.html#determining-the-character-encoding * * @param String encoding Target encoding * @return String */ Body.prototype._convert = function(encoding) { encoding = encoding || 'utf-8'; var ct = this.headers.get('content-type'); var charset = 'utf-8'; var res, str; // header if (ct) { // skip encoding detection altogether if not html/xml/plain text if (!/text\/html|text\/plain|\+xml|\/xml/i.test(ct)) { return Buffer.concat(this._raw); } res = /charset=([^;]*)/i.exec(ct); } // no charset in content type, peek at response body for at most 1024 bytes if (!res && this._raw.length > 0) { for (var i = 0; i < this._raw.length; i++) { str += this._raw[i].toString() if (str.length > 1024) { break; } } str = str.substr(0, 1024); } // html5 if (!res && str) { res = /