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:
commit
b21dd5a15b
3
index.js
3
index.js
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
/**
|
||||
* headers.js
|
||||
*
|
||||
|
@ -137,4 +136,4 @@ Headers.prototype['delete'] = function(name) {
|
|||
*/
|
||||
Headers.prototype.raw = function() {
|
||||
return this._headers;
|
||||
};
|
||||
};
|
|
@ -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);
|
||||
|
|
156
lib/response.js
156
lib/response.js
|
@ -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;
|
||||
|
|
32
test/test.js
32
test/test.js
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue