/** * 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 = /<meta.+?charset=(['"])(.+?)\1/i.exec(str); } // html4 if (!res && str) { res = /<meta[\s]+?http-equiv=(['"])content-type\1[\s]+?content=(['"])(.+?)\2/i.exec(str); if (res) { res = /charset=(.*)/i.exec(res.pop()); } } // xml if (!res && str) { res = /<\?xml.+?encoding=(['"])(.+?)\1/i.exec(str); } // found charset if (res) { charset = res.pop(); // prevent decode issues when sites use incorrect encoding // ref: https://hsivonen.fi/encoding-menu/ if (charset === 'gb2312' || charset === 'gbk') { charset = 'gb18030'; } } // turn raw buffers into a single utf-8 buffer return convert( Buffer.concat(this._raw) , encoding , charset ); }; /** * Clone body given Res/Req instance * * @param Mixed instance Response or Request instance * @return Mixed */ Body.prototype._clone = function(instance) { var p1, p2; var body = instance.body; // don't allow cloning a used body if (instance.bodyUsed) { throw new Error('cannot clone body after it is used'); } // check that body is a stream and not form-data object // note: we can't clone the form-data object without having it as a dependency if (bodyStream(body) && typeof body.getBoundary !== 'function') { // tee instance body p1 = new PassThrough(); p2 = new PassThrough(); body.pipe(p1); body.pipe(p2); // set instance body to teed body and return the other teed body instance.body = p1; body = p2; } return body; } // expose Promise Body.Promise = global.Promise;