Merge pull request #39 from kirill-konshin/master

Refactor Body, so both Request and Response finally has common Body methods like text/json
This commit is contained in:
David Frank 2016-02-18 14:06:08 +08:00
commit b21dd5a15b
6 changed files with 239 additions and 159 deletions

View File

@ -12,6 +12,7 @@ var https = require('https');
var zlib = require('zlib');
var stream = require('stream');
var Body = require('./lib/body');
var Response = require('./lib/response');
var Headers = require('./lib/headers');
var Request = require('./lib/request');
@ -36,7 +37,7 @@ function Fetch(url, opts) {
throw new Error('native promise missing, set Fetch.Promise to your favorite alternative');
}
Response.Promise = Fetch.Promise;
Body.Promise = Fetch.Promise;
var self = this;

192
lib/body.js Normal file
View File

@ -0,0 +1,192 @@
/**
* response.js
*
* Response class provides content decoding
*/
var convert = require('encoding').convert;
module.exports = Body;
/**
* Response 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() {
return this._decode().then(function(text) {
return JSON.parse(text);
});
};
/**
* Decode response as text
*
* @return Promise
*/
Body.prototype.text = 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;
if (typeof self.body === 'string') {
self._bytes = self.body.length;
self._raw = [new Buffer(self.body)];
return resolve(self._convert());
}
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 Error('response timeout at ' + self.url + ' over limit: ' + self.timeout));
}, self.timeout);
}
// handle stream error, such as incorrect content-encoding
self.body.on('error', function(err) {
reject(new Error('invalid response body at: ' + self.url + ' reason: ' + err.message));
});
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 Error('content size at ' + self.url + ' over limit: ' + self.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 charset = 'utf-8';
var res, str;
// header
if (this.headers.has('content-type')) {
res = /charset=([^;]*)/i.exec(this.headers.get('content-type'));
}
// no charset in content type, peek at response body
if (!res && this._raw.length > 0) {
str = this._raw[0].toString().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 utf-8 string
return convert(
Buffer.concat(this._raw)
, encoding
, charset
).toString();
};
// expose Promise
Body.Promise = global.Promise;

View File

@ -1,4 +1,3 @@
/**
* headers.js
*
@ -137,4 +136,4 @@ Headers.prototype['delete'] = function(name) {
*/
Headers.prototype.raw = function() {
return this._headers;
};
};

View File

@ -1,4 +1,3 @@
/**
* request.js
*
@ -7,6 +6,7 @@
var parse_url = require('url').parse;
var Headers = require('./headers');
var Body = require('./body');
module.exports = Request;
@ -44,7 +44,6 @@ function Request(input, init) {
// fetch spec options
this.method = init.method || input.method || 'GET';
this.headers = new Headers(init.headers || input.headers || {});
this.body = init.body || input.body;
this.url = url;
// server only options
@ -52,13 +51,16 @@ function Request(input, init) {
init.follow : input.follow !== undefined ?
input.follow : 20;
this.counter = init.counter || input.follow || 0;
this.timeout = init.timeout || input.timeout || 0;
this.compress = init.compress !== undefined ?
init.compress : input.compress !== undefined ?
input.compress : true;
this.size = init.size || input.size || 0;
this.agent = init.agent || input.agent;
Body.call(this, init.body || input.body, {
timeout: init.timeout || input.timeout || 0,
size: init.size || input.size || 0
});
// server request options
this.protocol = url_parsed.protocol;
this.hostname = url_parsed.hostname;
@ -66,3 +68,5 @@ function Request(input, init) {
this.path = url_parsed.path;
this.auth = url_parsed.auth;
}
Request.prototype = Object.create(Body.prototype);

View File

@ -1,4 +1,3 @@
/**
* response.js
*
@ -8,6 +7,7 @@
var http = require('http');
var convert = require('encoding').convert;
var Headers = require('./headers');
var Body = require('./body');
module.exports = Response;
@ -26,161 +26,13 @@ function Response(body, opts) {
this.status = opts.status;
this.statusText = http.STATUS_CODES[this.status];
this.headers = new Headers(opts.headers);
this.body = body;
this.bodyUsed = false;
this.size = opts.size;
this.ok = this.status >= 200 && this.status < 300;
this.timeout = opts.timeout;
Body.call(this, body, opts);
}
/**
* Decode response as json
*
* @return Promise
*/
Response.prototype.json = function() {
return this._decode().then(function(text) {
return JSON.parse(text);
});
}
/**
* Decode response as text
*
* @return Promise
*/
Response.prototype.text = function() {
return this._decode();
}
/**
* Decode buffers into utf-8 string
*
* @return Promise
*/
Response.prototype._decode = function() {
var self = this;
if (this.bodyUsed) {
return Response.Promise.reject(new Error('body used already for: ' + this.url));
}
this.bodyUsed = true;
this._bytes = 0;
this._abort = false;
this._raw = [];
return new Response.Promise(function(resolve, reject) {
var resTimeout;
// allow timeout on slow response body
if (self.timeout) {
resTimeout = setTimeout(function() {
self._abort = true;
reject(new Error('response timeout at ' + self.url + ' over limit: ' + self.timeout));
}, self.timeout);
}
// handle stream error, such as incorrect content-encoding
self.body.on('error', function(err) {
reject(new Error('invalid response body at: ' + self.url + ' reason: ' + err.message));
});
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 Error('content size at ' + self.url + ' over limit: ' + self.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
*/
Response.prototype._convert = function(encoding) {
encoding = encoding || 'utf-8';
var charset = 'utf-8';
var res, str;
// header
if (this.headers.has('content-type')) {
res = /charset=([^;]*)/i.exec(this.headers.get('content-type'));
}
// no charset in content type, peek at response body
if (!res && this._raw.length > 0) {
str = this._raw[0].toString().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 utf-8 string
return convert(
Buffer.concat(this._raw)
, encoding
, charset
).toString();
}
Response.prototype = Object.create(Body.prototype);
// expose Promise
Response.Promise = global.Promise;

View File

@ -927,4 +927,36 @@ describe('node-fetch', function() {
});
});
it('should support parsing headers in Response constructor', function(){
var r = new Response(null, {headers: {'foo': 'bar'}});
expect(r.headers.get('foo')).to.equal('bar');
});
it('should support parsing headers in Request constructor', function(){
var r = new Request('http://foo', {headers: {'foo': 'bar'}});
expect(r.headers.get('foo')).to.equal('bar');
});
it('should support string bodies in Response constructor', function() {
return (new Response('foo')).text().then(function(text) {
expect(text).to.equal('foo');
});
});
it('should support text() method in Request class', function() {
return (new Request('http://foo', {body: 'foo'})).text().then(function(text) {
expect(text).to.equal('foo');
});
});
it('should support json() method in Request class', function() {
return (new Request('http://foo', {body: '{"foo":"bar"}'})).json().then(function(json) {
expect(json.foo).to.equal('bar');
});
});
});