2017-05-03 15:35:00 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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() {
|
|
|
|
|
2017-05-24 15:10:37 +02:00
|
|
|
var self = this;
|
2017-05-03 15:35:00 +02:00
|
|
|
|
|
|
|
return this._decode().then(function(buffer) {
|
2017-05-24 15:10:37 +02:00
|
|
|
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'));
|
|
|
|
}
|
2017-05-03 15:35:00 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|