Merge pull request #121 from bitinn/bugfix
closes #115 #105 #102 #101 #43
This commit is contained in:
commit
d47549eab1
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -5,7 +5,15 @@ Changelog
|
||||||
|
|
||||||
# 1.x release
|
# 1.x release
|
||||||
|
|
||||||
## v1.5.2 (master)
|
## v1.5.3 (master)
|
||||||
|
|
||||||
|
- Fix: handles 204 and 304 responses when body is empty but content-encoding is gzip/deflate
|
||||||
|
- Fix: allow resolving response and cloned response in any order
|
||||||
|
- Fix: avoid setting content-length when form-data body use streams
|
||||||
|
- Fix: send DELETE request with content-length when body is present
|
||||||
|
- Fix: allow any url when calling new Request, but still reject non-http(s) url in fetch
|
||||||
|
|
||||||
|
## v1.5.2
|
||||||
|
|
||||||
- Fix: allow node.js core to handle keep-alive connection pool when passing a custom agent
|
- Fix: allow node.js core to handle keep-alive connection pool when passing a custom agent
|
||||||
|
|
||||||
|
|
31
index.js
31
index.js
|
@ -48,12 +48,14 @@ function Fetch(url, opts) {
|
||||||
// wrap http.request into fetch
|
// wrap http.request into fetch
|
||||||
return new Fetch.Promise(function(resolve, reject) {
|
return new Fetch.Promise(function(resolve, reject) {
|
||||||
// build request object
|
// build request object
|
||||||
var options;
|
var options = new Request(url, opts);
|
||||||
try {
|
|
||||||
options = new Request(url, opts);
|
if (!options.protocol || !options.hostname) {
|
||||||
} catch (err) {
|
throw new Error('only absolute urls are supported');
|
||||||
reject(err);
|
}
|
||||||
return;
|
|
||||||
|
if (options.protocol !== 'http:' && options.protocol !== 'https:') {
|
||||||
|
throw new Error('only http(s) protocols are supported');
|
||||||
}
|
}
|
||||||
|
|
||||||
var send;
|
var send;
|
||||||
|
@ -87,12 +89,12 @@ function Fetch(url, opts) {
|
||||||
headers.set('content-type', 'multipart/form-data; boundary=' + options.body.getBoundary());
|
headers.set('content-type', 'multipart/form-data; boundary=' + options.body.getBoundary());
|
||||||
}
|
}
|
||||||
|
|
||||||
// bring node-fetch closer to browser behavior by setting content-length automatically for POST, PUT, PATCH requests when body is empty or string
|
// bring node-fetch closer to browser behavior by setting content-length automatically
|
||||||
if (!headers.has('content-length') && options.method.substr(0, 1).toUpperCase() === 'P') {
|
if (!headers.has('content-length') && /post|put|patch|delete/i.test(options.method)) {
|
||||||
if (typeof options.body === 'string') {
|
if (typeof options.body === 'string') {
|
||||||
headers.set('content-length', Buffer.byteLength(options.body));
|
headers.set('content-length', Buffer.byteLength(options.body));
|
||||||
// detect form data input from form-data module, this hack avoid the need to add content-length header manually
|
// detect form data input from form-data module, this hack avoid the need to add content-length header manually
|
||||||
} else if (options.body && typeof options.body.getLengthSync === 'function') {
|
} else if (options.body && typeof options.body.getLengthSync === 'function' && options.body._lengthRetrievers.length == 0) {
|
||||||
headers.set('content-length', options.body.getLengthSync().toString());
|
headers.set('content-length', options.body.getLengthSync().toString());
|
||||||
// this is only necessary for older nodejs releases (before iojs merge)
|
// this is only necessary for older nodejs releases (before iojs merge)
|
||||||
} else if (options.body === undefined || options.body === null) {
|
} else if (options.body === undefined || options.body === null) {
|
||||||
|
@ -167,10 +169,13 @@ function Fetch(url, opts) {
|
||||||
if (options.compress && headers.has('content-encoding')) {
|
if (options.compress && headers.has('content-encoding')) {
|
||||||
var name = headers.get('content-encoding');
|
var name = headers.get('content-encoding');
|
||||||
|
|
||||||
if (name == 'gzip' || name == 'x-gzip') {
|
// no need to pipe no content and not modified response body
|
||||||
body = body.pipe(zlib.createGunzip());
|
if (res.statusCode !== 204 && res.statusCode !== 304) {
|
||||||
} else if (name == 'deflate' || name == 'x-deflate') {
|
if (name == 'gzip' || name == 'x-gzip') {
|
||||||
body = body.pipe(zlib.createInflate());
|
body = body.pipe(zlib.createGunzip());
|
||||||
|
} else if (name == 'deflate' || name == 'x-deflate') {
|
||||||
|
body = body.pipe(zlib.createInflate());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
13
lib/body.js
13
lib/body.js
|
@ -205,7 +205,7 @@ Body.prototype._convert = function(encoding) {
|
||||||
* @return Mixed
|
* @return Mixed
|
||||||
*/
|
*/
|
||||||
Body.prototype._clone = function(instance) {
|
Body.prototype._clone = function(instance) {
|
||||||
var pass;
|
var p1, p2;
|
||||||
var body = instance.body;
|
var body = instance.body;
|
||||||
|
|
||||||
// don't allow cloning a used body
|
// don't allow cloning a used body
|
||||||
|
@ -216,9 +216,14 @@ Body.prototype._clone = function(instance) {
|
||||||
// check that body is a stream and not form-data object
|
// 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
|
// note: we can't clone the form-data object without having it as a dependency
|
||||||
if (bodyStream(body) && typeof body.getBoundary !== 'function') {
|
if (bodyStream(body) && typeof body.getBoundary !== 'function') {
|
||||||
pass = new PassThrough();
|
// tee instance body
|
||||||
body.pipe(pass);
|
p1 = new PassThrough();
|
||||||
body = pass;
|
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;
|
return body;
|
||||||
|
|
|
@ -31,14 +31,6 @@ function Request(input, init) {
|
||||||
url_parsed = parse_url(url);
|
url_parsed = parse_url(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!url_parsed.protocol || !url_parsed.hostname) {
|
|
||||||
throw new Error('only absolute urls are supported');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url_parsed.protocol !== 'http:' && url_parsed.protocol !== 'https:') {
|
|
||||||
throw new Error('only http(s) protocols are supported');
|
|
||||||
}
|
|
||||||
|
|
||||||
// normalize init
|
// normalize init
|
||||||
init = init || {};
|
init = init || {};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
i am a dummy
|
|
@ -264,11 +264,28 @@ TestServer.prototype.router = function(req, res) {
|
||||||
res.end('invalid json');
|
res.end('invalid json');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (p === '/empty') {
|
if (p === '/no-content') {
|
||||||
res.statusCode = 204;
|
res.statusCode = 204;
|
||||||
res.end();
|
res.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (p === '/no-content/gzip') {
|
||||||
|
res.statusCode = 204;
|
||||||
|
res.setHeader('Content-Encoding', 'gzip');
|
||||||
|
res.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p === '/not-modified') {
|
||||||
|
res.statusCode = 304;
|
||||||
|
res.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p === '/not-modified/gzip') {
|
||||||
|
res.statusCode = 304;
|
||||||
|
res.setHeader('Content-Encoding', 'gzip');
|
||||||
|
res.end();
|
||||||
|
}
|
||||||
|
|
||||||
if (p === '/inspect') {
|
if (p === '/inspect') {
|
||||||
res.statusCode = 200;
|
res.statusCode = 200;
|
||||||
res.setHeader('Content-Type', 'application/json');
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
|
122
test/test.js
122
test/test.js
|
@ -11,6 +11,7 @@ var stream = require('stream');
|
||||||
var resumer = require('resumer');
|
var resumer = require('resumer');
|
||||||
var FormData = require('form-data');
|
var FormData = require('form-data');
|
||||||
var http = require('http');
|
var http = require('http');
|
||||||
|
var fs = require('fs');
|
||||||
|
|
||||||
var TestServer = require('./server');
|
var TestServer = require('./server');
|
||||||
|
|
||||||
|
@ -416,8 +417,8 @@ describe('node-fetch', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle empty response', function() {
|
it('should handle no content response', function() {
|
||||||
url = base + '/empty';
|
url = base + '/no-content';
|
||||||
return fetch(url).then(function(res) {
|
return fetch(url).then(function(res) {
|
||||||
expect(res.status).to.equal(204);
|
expect(res.status).to.equal(204);
|
||||||
expect(res.statusText).to.equal('No Content');
|
expect(res.statusText).to.equal('No Content');
|
||||||
|
@ -429,6 +430,47 @@ describe('node-fetch', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle no content response with gzip encoding', function() {
|
||||||
|
url = base + '/no-content/gzip';
|
||||||
|
return fetch(url).then(function(res) {
|
||||||
|
expect(res.status).to.equal(204);
|
||||||
|
expect(res.statusText).to.equal('No Content');
|
||||||
|
expect(res.headers.get('content-encoding')).to.equal('gzip');
|
||||||
|
expect(res.ok).to.be.true;
|
||||||
|
return res.text().then(function(result) {
|
||||||
|
expect(result).to.be.a('string');
|
||||||
|
expect(result).to.be.empty;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle not modified response', function() {
|
||||||
|
url = base + '/not-modified';
|
||||||
|
return fetch(url).then(function(res) {
|
||||||
|
expect(res.status).to.equal(304);
|
||||||
|
expect(res.statusText).to.equal('Not Modified');
|
||||||
|
expect(res.ok).to.be.false;
|
||||||
|
return res.text().then(function(result) {
|
||||||
|
expect(result).to.be.a('string');
|
||||||
|
expect(result).to.be.empty;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle not modified response with gzip encoding', function() {
|
||||||
|
url = base + '/not-modified/gzip';
|
||||||
|
return fetch(url).then(function(res) {
|
||||||
|
expect(res.status).to.equal(304);
|
||||||
|
expect(res.statusText).to.equal('Not Modified');
|
||||||
|
expect(res.headers.get('content-encoding')).to.equal('gzip');
|
||||||
|
expect(res.ok).to.be.false;
|
||||||
|
return res.text().then(function(result) {
|
||||||
|
expect(result).to.be.a('string');
|
||||||
|
expect(result).to.be.empty;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should decompress gzip response', function() {
|
it('should decompress gzip response', function() {
|
||||||
url = base + '/gzip';
|
url = base + '/gzip';
|
||||||
return fetch(url).then(function(res) {
|
return fetch(url).then(function(res) {
|
||||||
|
@ -566,10 +608,13 @@ describe('node-fetch', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow POST request with readable stream as body', function() {
|
it('should allow POST request with readable stream as body', function() {
|
||||||
|
var body = resumer().queue('a=1').end();
|
||||||
|
body = body.pipe(new stream.PassThrough());
|
||||||
|
|
||||||
url = base + '/inspect';
|
url = base + '/inspect';
|
||||||
opts = {
|
opts = {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
, body: resumer().queue('a=1').end()
|
, body: body
|
||||||
};
|
};
|
||||||
return fetch(url, opts).then(function(res) {
|
return fetch(url, opts).then(function(res) {
|
||||||
return res.json();
|
return res.json();
|
||||||
|
@ -600,6 +645,26 @@ describe('node-fetch', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should allow POST request with form-data using stream as body', function() {
|
||||||
|
var form = new FormData();
|
||||||
|
form.append('my_field', fs.createReadStream('test/dummy.txt'));
|
||||||
|
|
||||||
|
url = base + '/multipart';
|
||||||
|
opts = {
|
||||||
|
method: 'POST'
|
||||||
|
, body: form
|
||||||
|
};
|
||||||
|
|
||||||
|
return fetch(url, opts).then(function(res) {
|
||||||
|
return res.json();
|
||||||
|
}).then(function(res) {
|
||||||
|
expect(res.method).to.equal('POST');
|
||||||
|
expect(res.headers['content-type']).to.contain('multipart/form-data');
|
||||||
|
expect(res.headers['content-length']).to.be.undefined;
|
||||||
|
expect(res.body).to.contain('my_field=');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should allow POST request with form-data as body and custom headers', function() {
|
it('should allow POST request with form-data as body and custom headers', function() {
|
||||||
var form = new FormData();
|
var form = new FormData();
|
||||||
form.append('a','1');
|
form.append('a','1');
|
||||||
|
@ -650,6 +715,38 @@ describe('node-fetch', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should allow POST request with string body', function() {
|
||||||
|
url = base + '/inspect';
|
||||||
|
opts = {
|
||||||
|
method: 'POST'
|
||||||
|
, body: 'a=1'
|
||||||
|
};
|
||||||
|
return fetch(url, opts).then(function(res) {
|
||||||
|
return res.json();
|
||||||
|
}).then(function(res) {
|
||||||
|
expect(res.method).to.equal('POST');
|
||||||
|
expect(res.body).to.equal('a=1');
|
||||||
|
expect(res.headers['transfer-encoding']).to.be.undefined;
|
||||||
|
expect(res.headers['content-length']).to.equal('3');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow DELETE request with string body', function() {
|
||||||
|
url = base + '/inspect';
|
||||||
|
opts = {
|
||||||
|
method: 'DELETE'
|
||||||
|
, body: 'a=1'
|
||||||
|
};
|
||||||
|
return fetch(url, opts).then(function(res) {
|
||||||
|
return res.json();
|
||||||
|
}).then(function(res) {
|
||||||
|
expect(res.method).to.equal('DELETE');
|
||||||
|
expect(res.body).to.equal('a=1');
|
||||||
|
expect(res.headers['transfer-encoding']).to.be.undefined;
|
||||||
|
expect(res.headers['content-length']).to.equal('3');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should allow PATCH request', function() {
|
it('should allow PATCH request', function() {
|
||||||
url = base + '/inspect';
|
url = base + '/inspect';
|
||||||
opts = {
|
opts = {
|
||||||
|
@ -898,6 +995,19 @@ describe('node-fetch', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should allow cloning a json response, first log as text response, then return json object', function() {
|
||||||
|
url = base + '/json';
|
||||||
|
return fetch(url).then(function(res) {
|
||||||
|
var r1 = res.clone();
|
||||||
|
return r1.text().then(function(result) {
|
||||||
|
expect(result).to.equal('{"name":"value"}');
|
||||||
|
return res.json().then(function(result) {
|
||||||
|
expect(result).to.deep.equal({name: 'value'});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should not allow cloning a response after its been used', function() {
|
it('should not allow cloning a response after its been used', function() {
|
||||||
url = base + '/hello';
|
url = base + '/hello';
|
||||||
return fetch(url).then(function(res) {
|
return fetch(url).then(function(res) {
|
||||||
|
@ -1197,6 +1307,12 @@ describe('node-fetch', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support arbitrary url in Request constructor', function() {
|
||||||
|
url = 'anything';
|
||||||
|
var req = new Request(url);
|
||||||
|
expect(req.url).to.equal('anything');
|
||||||
|
});
|
||||||
|
|
||||||
it('should support clone() method in Request constructor', function() {
|
it('should support clone() method in Request constructor', function() {
|
||||||
url = base;
|
url = base;
|
||||||
var body = resumer().queue('a=1').end();
|
var body = resumer().queue('a=1').end();
|
||||||
|
|
Loading…
Reference in New Issue