From 79d2e2cc3704679e2d9a7864231317252a6c47ac Mon Sep 17 00:00:00 2001 From: David Frank Date: Sat, 19 Mar 2016 14:41:19 +0800 Subject: [PATCH 01/16] bump dependencies and fix incompatible tests --- package.json | 10 +++++----- test/test.js | 1 - 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index b58358d..7879cff 100644 --- a/package.json +++ b/package.json @@ -24,15 +24,15 @@ }, "homepage": "https://github.com/bitinn/node-fetch", "devDependencies": { - "bluebird": "^2.9.1", - "chai": "^1.10.0", - "chai-as-promised": "^4.1.1", + "bluebird": "^3.3.4", + "chai": "^3.5.0", + "chai-as-promised": "^5.2.0", "coveralls": "^2.11.2", "form-data": "^1.0.0-rc1", - "istanbul": "^0.3.5", + "istanbul": "^0.4.2", "mocha": "^2.1.0", "parted": "^0.1.1", - "promise": "^6.1.0", + "promise": "^7.1.1", "resumer": "0.0.0" }, "dependencies": { diff --git a/test/test.js b/test/test.js index 813e8c9..9ca54fb 100644 --- a/test/test.js +++ b/test/test.js @@ -824,7 +824,6 @@ describe('node-fetch', function() { expect(h1._headers['a']).to.include('1'); expect(h1._headers['a']).to.not.include('2'); - expect(h1._headers['b']).to.not.include('1'); expect(h2._headers['a']).to.include('1'); expect(h2._headers['a']).to.not.include('2'); From 4624f41385c1f0d44b275d40a484ac82d17134df Mon Sep 17 00:00:00 2001 From: David Frank Date: Sat, 19 Mar 2016 15:11:16 +0800 Subject: [PATCH 02/16] fix coverage and remove duplicate tests --- test/test.js | 99 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 64 insertions(+), 35 deletions(-) diff --git a/test/test.js b/test/test.js index 9ca54fb..f277f58 100644 --- a/test/test.js +++ b/test/test.js @@ -18,6 +18,7 @@ var fetch = require('../index.js'); var Headers = require('../lib/headers.js'); var Response = require('../lib/response.js'); var Request = require('../lib/request.js'); +var Body = require('../lib/body.js'); // test with native promise on node 0.11, and bluebird for node 0.10 fetch.Promise = fetch.Promise || bluebird; @@ -890,14 +891,46 @@ describe('node-fetch', function() { }); it('should support parsing headers in Response constructor', function() { - var body = resumer().queue('a=1').end(); - body = body.pipe(new stream.PassThrough()); - var res = new Response(body, { + var res = new Response(null, { headers: { a: '1' } }); expect(res.headers.get('a')).to.equal('1'); + }); + + it('should support text() method in Response constructor', function() { + var res = new Response('a=1'); + return res.text().then(function(result) { + expect(result).to.equal('a=1'); + }); + }); + + it('should support json() method in Response constructor', function() { + var res = new Response('{"a":1}'); + return res.json().then(function(result) { + expect(result.a).to.equal(1); + }); + }); + + it('should support stream as body in Response constructor', function() { + var body = resumer().queue('a=1').end(); + body = body.pipe(new stream.PassThrough()); + var res = new Response(body); + return res.text().then(function(result) { + expect(result).to.equal('a=1'); + }); + }); + + it('should support string as body in Response constructor', function() { + var res = new Response('a=1'); + return res.text().then(function(result) { + expect(result).to.equal('a=1'); + }); + }); + + it('should support buffer as body in Response constructor', function() { + var res = new Response(new Buffer('a=1')); return res.text().then(function(result) { expect(result).to.equal('a=1'); }); @@ -914,6 +947,34 @@ describe('node-fetch', function() { expect(req.headers.get('a')).to.equal('1'); }); + it('should support text() method in Request constructor', function() { + url = base; + var req = new Request(url, { + body: 'a=1' + }); + expect(req.url).to.equal(url); + return req.text().then(function(result) { + expect(result).to.equal('a=1'); + }); + });
 + + it('should support json() method in Request constructor', function() { + url = base; + var req = new Request(url, { + body: '{"a":1}' + }); + expect(req.url).to.equal(url); + return req.json().then(function(result) { + expect(result.a).to.equal(1); + }); + });
 + + it('should support text() and json() method in Body constructor', function() { + var body = new Body('a=1'); + expect(body).to.have.property('text'); + expect(body).to.have.property('json'); + });
 + it('should support https request', function() { this.timeout(5000); url = 'https://github.com/'; @@ -926,36 +987,4 @@ 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'); - }); - });
 - }); From c3a4e96a61b935fd42487ca6bea3caf4fc82566a Mon Sep 17 00:00:00 2001 From: David Frank Date: Sat, 19 Mar 2016 15:33:13 +0800 Subject: [PATCH 03/16] more test cleanup --- test/test.js | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/test/test.js b/test/test.js index f277f58..3a5b5f9 100644 --- a/test/test.js +++ b/test/test.js @@ -738,25 +738,24 @@ describe('node-fetch', function() { it('should allow iterating through all headers', function() { var headers = new Headers({ - a: 1, - b: [2, 3], + a: 1 + , b: [2, 3] + , c: [4] + }); + expect(headers).to.have.property('forEach'); + + var result = []; + headers.forEach(function(val, key) { + result.push([key, val]); }); - var myHeaders = []; - function callback(value, name) { - myHeaders.push([name, value]); - } - expected = [ - ["a", "1"], - ["b", "2"], - ["b", "3"] + ["a", "1"] + , ["b", "2"] + , ["b", "3"] + , ["c", "4"] ]; - - expect(headers.forEach).to.be.defined; - - headers.forEach(callback, headers); - expect(myHeaders).to.be.deep.equal(expected); + expect(result).to.be.deep.equal(expected); }); it('should allow deleting header', function() { From 054fbb4d8cf70e103228ab945656d1134427a969 Mon Sep 17 00:00:00 2001 From: David Frank Date: Sat, 19 Mar 2016 15:34:14 +0800 Subject: [PATCH 04/16] no longer test for deprecated iojs --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 692c2d6..1faa00a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,6 @@ language: node_js node_js: - "0.10" - "0.12" - - "iojs" - "node" before_install: npm install -g npm script: npm run coverage \ No newline at end of file From 532adab676dc8fbb9d9616c6756a167bc097c0f1 Mon Sep 17 00:00:00 2001 From: David Frank Date: Sat, 19 Mar 2016 15:41:25 +0800 Subject: [PATCH 05/16] fix code formating --- lib/body.js | 208 ++++++++++++++++++++++++------------------------- lib/headers.js | 13 ++-- 2 files changed, 111 insertions(+), 110 deletions(-) diff --git a/lib/body.js b/lib/body.js index 7c15f91..847eb0f 100644 --- a/lib/body.js +++ b/lib/body.js @@ -17,14 +17,14 @@ module.exports = Body; */ function Body(body, opts) { - opts = opts || {}; + opts = opts || {}; - this.body = body; - this.bodyUsed = false; - this.size = opts.size || 0; - this.timeout = opts.timeout || 0; - this._raw = []; - this._abort = false; + this.body = body; + this.bodyUsed = false; + this.size = opts.size || 0; + this.timeout = opts.timeout || 0; + this._raw = []; + this._abort = false; } @@ -35,9 +35,9 @@ function Body(body, opts) { */ Body.prototype.json = function() { - return this._decode().then(function(text) { - return JSON.parse(text); - }); + return this._decode().then(function(text) { + return JSON.parse(text); + }); }; @@ -48,7 +48,7 @@ Body.prototype.json = function() { */ Body.prototype.text = function() { - return this._decode(); + return this._decode(); }; @@ -59,69 +59,69 @@ Body.prototype.text = function() { */ Body.prototype._decode = function() { - var self = this; + var self = this; - if (this.bodyUsed) { - return Body.Promise.reject(new Error('body used already for: ' + this.url)); - } + 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 = []; + this.bodyUsed = true; + this._bytes = 0; + this._abort = false; + this._raw = []; - return new Body.Promise(function(resolve, reject) { - var resTimeout; + 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 (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()); - } + 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); - } + // 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)); - }); + // 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; - } + 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; - } + 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._bytes += chunk.length; + self._raw.push(chunk); + }); - self.body.on('end', function() { - if (self._abort) { - return; - } + self.body.on('end', function() { + if (self._abort) { + return; + } - clearTimeout(resTimeout); - resolve(self._convert()); - }); - }); + clearTimeout(resTimeout); + resolve(self._convert()); + }); + }); }; @@ -134,59 +134,59 @@ Body.prototype._decode = function() { */ Body.prototype._convert = function(encoding) { - encoding = encoding || 'utf-8'; + encoding = encoding || 'utf-8'; - var charset = 'utf-8'; - var res, str; + var charset = 'utf-8'; + var res, str; - // header - if (this.headers.has('content-type')) { - res = /charset=([^;]*)/i.exec(this.headers.get('content-type')); - } + // 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); - } + // 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 = / Date: Sat, 19 Mar 2016 15:46:23 +0800 Subject: [PATCH 06/16] no longer need to expose promise on reponse constructor now that body constructor exists --- lib/response.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/response.js b/lib/response.js index 04d4d7f..0586b19 100644 --- a/lib/response.js +++ b/lib/response.js @@ -33,6 +33,3 @@ function Response(body, opts) { } Response.prototype = Object.create(Body.prototype); - -// expose Promise -Response.Promise = global.Promise; From 85c18162ac716af165c24e702269e6ec0cba8821 Mon Sep 17 00:00:00 2001 From: David Frank Date: Sat, 19 Mar 2016 15:51:48 +0800 Subject: [PATCH 07/16] test for form-data content-length hack --- test/test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/test.js b/test/test.js index 3a5b5f9..f90e22e 100644 --- a/test/test.js +++ b/test/test.js @@ -524,6 +524,7 @@ describe('node-fetch', function() { }).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.a('string'); expect(res.body).to.equal('a=1'); }); }); @@ -546,6 +547,7 @@ describe('node-fetch', function() { }).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.a('string'); expect(res.headers.b).to.equal('2'); expect(res.body).to.equal('a=1'); }); From a743f5f6995a0d397d9188dbae20a83d38c26c60 Mon Sep 17 00:00:00 2001 From: David Frank Date: Sat, 19 Mar 2016 15:55:33 +0800 Subject: [PATCH 08/16] explain the default export line --- index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/index.js b/index.js index 2512b31..1f46b26 100644 --- a/index.js +++ b/index.js @@ -17,7 +17,9 @@ var Response = require('./lib/response'); var Headers = require('./lib/headers'); var Request = require('./lib/request'); +// commonjs module.exports = Fetch; +// es6 default export compatibility module.exports.default = module.exports; /** From 00fa8679148b7cbc4279022dc47b6e4ea097baa5 Mon Sep 17 00:00:00 2001 From: David Frank Date: Sat, 19 Mar 2016 16:17:14 +0800 Subject: [PATCH 09/16] fix chunked encoding support for character encoding conversion, thx @dsuket PR #50 --- lib/body.js | 10 ++++++++-- test/server.js | 11 +++++++++++ test/test.js | 11 +++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/lib/body.js b/lib/body.js index 847eb0f..773b982 100644 --- a/lib/body.js +++ b/lib/body.js @@ -144,9 +144,15 @@ Body.prototype._convert = function(encoding) { res = /charset=([^;]*)/i.exec(this.headers.get('content-type')); } - // no charset in content type, peek at response body + // no charset in content type, peek at response body for at most 1024 bytes if (!res && this._raw.length > 0) { - str = this._raw[0].toString().substr(0, 1024); + 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 diff --git a/test/server.js b/test/server.js index 7403922..6e03f0c 100644 --- a/test/server.js +++ b/test/server.js @@ -168,6 +168,17 @@ TestServer.prototype.router = function(req, res) { res.end(convert('中文', 'gbk')); } + if (p === '/encoding/chunked') { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/html'); + res.setHeader('Transfer-Encoding', 'chunked'); + var padding = 'a'; + for (var i = 0; i < 10; i++) { + res.write(padding); + } + res.end(convert('
日本語
', 'Shift_JIS')); + } + if (p === '/redirect/301') { res.statusCode = 301; res.setHeader('Location', '/inspect'); diff --git a/test/test.js b/test/test.js index f90e22e..170ca57 100644 --- a/test/test.js +++ b/test/test.js @@ -712,6 +712,17 @@ describe('node-fetch', function() { }); }); + it('should support chunked encoding, html4 detect', function() { + url = base + '/encoding/chunked'; + return fetch(url).then(function(res) { + expect(res.status).to.equal(200); + var padding = 'a'.repeat(10); + return res.text().then(function(result) { + expect(result).to.equal(padding + '
日本語
'); + }); + }); + }); + it('should allow piping response body as stream', function(done) { url = base + '/hello'; fetch(url).then(function(res) { From 362aa087caf1bdcb6eecce3d7b8faf494c74b1f6 Mon Sep 17 00:00:00 2001 From: David Frank Date: Sat, 19 Mar 2016 18:06:33 +0800 Subject: [PATCH 10/16] clone method support --- index.js | 1 + lib/body.js | 25 ++++++++++ lib/request.js | 15 ++++-- lib/response.js | 19 ++++++-- package.json | 3 +- test/server.js | 11 +++++ test/test.js | 118 +++++++++++++++++++++++++++++++++++++++++++++++- 7 files changed, 184 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index 1f46b26..02072dc 100644 --- a/index.js +++ b/index.js @@ -172,6 +172,7 @@ function Fetch(url, opts) { var output = new Response(body, { url: options.url , status: res.statusCode + , statusText: res.statusMessage , headers: headers , size: options.size , timeout: options.timeout diff --git a/lib/body.js b/lib/body.js index 773b982..fdc1139 100644 --- a/lib/body.js +++ b/lib/body.js @@ -5,6 +5,8 @@ */ var convert = require('encoding').convert; +var bodyStream = require('is-stream'); +var PassThrough = require('stream').PassThrough; module.exports = Body; @@ -194,5 +196,28 @@ Body.prototype._convert = function(encoding) { }; +/** + * Clone body given Res/Req instance + * + * @param Mixed instance Response or Request instance + * @return Mixed + */ +Body.prototype._clone = function(instance) { + var pass; + var body = instance.body; + + if (instance.bodyUsed) { + throw new Error('cannot clone body after it is used'); + } + + if (bodyStream(body)) { + pass = new PassThrough(); + body.pipe(pass); + body = pass; + } + + return body; +} + // expose Promise Body.Promise = global.Promise; diff --git a/lib/request.js b/lib/request.js index 33d7691..f6ab3af 100644 --- a/lib/request.js +++ b/lib/request.js @@ -50,13 +50,13 @@ function Request(input, init) { this.follow = init.follow !== undefined ? init.follow : input.follow !== undefined ? input.follow : 20; - this.counter = init.counter || input.follow || 0; this.compress = init.compress !== undefined ? init.compress : input.compress !== undefined ? input.compress : true; - this.agent = init.agent || input.agent; + this.counter = init.counter || input.counter || input.follow || 0; + this.agent = init.agent || input.agent || input.agent; - Body.call(this, init.body || input.body, { + Body.call(this, init.body || this._clone(input), { timeout: init.timeout || input.timeout || 0, size: init.size || input.size || 0 }); @@ -70,3 +70,12 @@ function Request(input, init) { } Request.prototype = Object.create(Body.prototype); + +/** + * Clone this request + * + * @return Request + */ +Request.prototype.clone = function() { + return new Request(this); +}; diff --git a/lib/response.js b/lib/response.js index 0586b19..adae440 100644 --- a/lib/response.js +++ b/lib/response.js @@ -4,8 +4,6 @@ * Response class provides content decoding */ -var http = require('http'); -var convert = require('encoding').convert; var Headers = require('./headers'); var Body = require('./body'); @@ -24,7 +22,7 @@ function Response(body, opts) { this.url = opts.url; this.status = opts.status; - this.statusText = http.STATUS_CODES[this.status]; + this.statusText = opts.statusText; this.headers = new Headers(opts.headers); this.ok = this.status >= 200 && this.status < 300; @@ -33,3 +31,18 @@ function Response(body, opts) { } Response.prototype = Object.create(Body.prototype); + +/** + * Clone this response + * + * @return Response + */ +Response.prototype.clone = function() { + return new Response(this._clone(this), { + url: this.url + , status: this.status + , statusText: this.statusText + , headers: this.headers + , ok: this.ok + }); +}; diff --git a/package.json b/package.json index 7879cff..a861bb1 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "resumer": "0.0.0" }, "dependencies": { - "encoding": "^0.1.11" + "encoding": "^0.1.11", + "is-stream": "^1.0.1" } } diff --git a/test/server.js b/test/server.js index 6e03f0c..36d7099 100644 --- a/test/server.js +++ b/test/server.js @@ -179,6 +179,17 @@ TestServer.prototype.router = function(req, res) { res.end(convert('
日本語
', 'Shift_JIS')); } + if (p === '/encoding/invalid') { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/html'); + res.setHeader('Transfer-Encoding', 'chunked'); + var padding = 'a'.repeat(120); + for (var i = 0; i < 10; i++) { + res.write(padding); + } + res.end(convert('中文', 'gbk')); + } + if (p === '/redirect/301') { res.statusCode = 301; res.setHeader('Location', '/inspect'); diff --git a/test/test.js b/test/test.js index 170ca57..cb3a398 100644 --- a/test/test.js +++ b/test/test.js @@ -10,6 +10,7 @@ var spawn = require('child_process').spawn; var stream = require('stream'); var resumer = require('resumer'); var FormData = require('form-data'); +var http = require('http'); var TestServer = require('./server'); @@ -723,6 +724,17 @@ describe('node-fetch', function() { }); }); + it('should only do encoding detection up to 1024 bytes', function() { + url = base + '/encoding/invalid'; + return fetch(url).then(function(res) { + expect(res.status).to.equal(200); + var padding = 'a'.repeat(1200); + return res.text().then(function(result) { + expect(result).to.not.equal(padding + '中文'); + }); + }); + }); + it('should allow piping response body as stream', function(done) { url = base + '/hello'; fetch(url).then(function(res) { @@ -739,6 +751,62 @@ describe('node-fetch', function() { }); }); + it('should allow cloning a response, and use both as stream', function(done) { + url = base + '/hello'; + return fetch(url).then(function(res) { + var counter = 0; + var r1 = res.clone(); + expect(res.body).to.be.an.instanceof(stream.Transform); + expect(r1.body).to.be.an.instanceof(stream.Transform); + res.body.on('data', function(chunk) { + if (chunk === null) { + return; + } + expect(chunk.toString()).to.equal('world'); + }); + res.body.on('end', function() { + counter++; + if (counter == 2) { + done(); + } + }); + r1.body.on('data', function(chunk) { + if (chunk === null) { + return; + } + expect(chunk.toString()).to.equal('world'); + }); + r1.body.on('end', function() { + counter++; + if (counter == 2) { + done(); + } + }); + }); + }); + + it('should allow cloning a json response, and log it as text response', function() { + url = base + '/json'; + return fetch(url).then(function(res) { + var r1 = res.clone(); + return fetch.Promise.all([r1.text(), res.json()]).then(function(results) { + expect(results[0]).to.equal('{"name":"value"}'); + expect(results[1]).to.deep.equal({name: 'value'}); + }); + }); + }); + + it('should not allow cloning a response after its been used', function() { + url = base + '/hello'; + return fetch(url).then(function(res) { + return res.text().then(function(result) { + expect(function() { + var r1 = res.clone(); + }).to.throw(Error); + }); + }) + }); + it('should allow get all responses of a header', function() { url = base + '/cookie'; return fetch(url).then(function(res) { @@ -768,7 +836,7 @@ describe('node-fetch', function() { , ["b", "3"] , ["c", "4"] ]; - expect(result).to.be.deep.equal(expected); + expect(result).to.deep.equal(expected); }); it('should allow deleting header', function() { @@ -925,6 +993,26 @@ describe('node-fetch', function() { }); }); + it('should support clone() method in Response constructor', function() { + var res = new Response('a=1', { + headers: { + a: '1' + } + , url: base + , status: 346 + , statusText: 'production' + }); + var cl = res.clone(); + expect(cl.headers.get('a')).to.equal('1'); + expect(cl.url).to.equal(base); + expect(cl.status).to.equal(346); + expect(cl.statusText).to.equal('production'); + expect(cl.ok).to.be.false; + return cl.text().then(function(result) { + expect(result).to.equal('a=1'); + }); + }); + it('should support stream as body in Response constructor', function() { var body = resumer().queue('a=1').end(); body = body.pipe(new stream.PassThrough()); @@ -981,6 +1069,34 @@ describe('node-fetch', function() { }); });
 + it('should support clone() method in Request constructor', function() { + url = base; + var agent = new http.Agent(); + var req = new Request(url, { + body: 'a=1' + , method: 'POST' + , headers: { + b: '2' + } + , follow: 3 + , compress: false + , agent: agent + }); + var cl = req.clone(); + expect(cl.url).to.equal(url); + expect(cl.method).to.equal('POST'); + expect(cl.headers.get('b')).to.equal('2'); + expect(cl.follow).to.equal(3); + expect(cl.compress).to.equal(false); + expect(cl.method).to.equal('POST'); + expect(cl.counter).to.equal(3); + expect(cl.agent).to.equal(agent); + return fetch.Promise.all([cl.text(), req.text()]).then(function(results) { + expect(results[0]).to.equal('a=1'); + expect(results[1]).to.equal('a=1'); + }); + }); + it('should support text() and json() method in Body constructor', function() { var body = new Body('a=1'); expect(body).to.have.property('text'); From 9a90e7d0b964dcf93c941378d8dfb862bcbc1113 Mon Sep 17 00:00:00 2001 From: David Frank Date: Sat, 19 Mar 2016 18:24:08 +0800 Subject: [PATCH 11/16] test for options support --- test/server.js | 6 ++++++ test/test.js | 13 +++++++++++++ 2 files changed, 19 insertions(+) diff --git a/test/server.js b/test/server.js index 36d7099..e529f2c 100644 --- a/test/server.js +++ b/test/server.js @@ -41,6 +41,12 @@ TestServer.prototype.router = function(req, res) { res.end('text'); } + if (p === '/options') { + res.statusCode = 200; + res.setHeader('Allow', ['GET', 'HEAD', 'OPTIONS']); + res.end('hello world'); + } + if (p === '/html') { res.statusCode = 200; res.setHeader('Content-Type', 'text/html'); diff --git a/test/test.js b/test/test.js index cb3a398..1d98b70 100644 --- a/test/test.js +++ b/test/test.js @@ -607,6 +607,19 @@ describe('node-fetch', function() { }); }); + it('should allow OPTIONS request', function() { + url = base + '/options'; + opts = { + method: 'OPTIONS' + }; + return fetch(url, opts).then(function(res) { + expect(res.status).to.equal(200); + expect(res.statusText).to.equal('OK'); + expect(res.headers.get('allow')).to.equal('GET, HEAD, OPTIONS'); + expect(res.body).to.be.an.instanceof(stream.Transform); + }); + }); + it('should reject decoding body twice', function() { url = base + '/plain'; return fetch(url).then(function(res) { From 827ce8fa31658b334a04df020f94ed2f90dda34f Mon Sep 17 00:00:00 2001 From: David Frank Date: Sat, 19 Mar 2016 18:36:53 +0800 Subject: [PATCH 12/16] fix tests for older node --- test/server.js | 3 ++- test/test.js | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/test/server.js b/test/server.js index e529f2c..262b638 100644 --- a/test/server.js +++ b/test/server.js @@ -189,7 +189,8 @@ TestServer.prototype.router = function(req, res) { res.statusCode = 200; res.setHeader('Content-Type', 'text/html'); res.setHeader('Transfer-Encoding', 'chunked'); - var padding = 'a'.repeat(120); + // because node v0.12 doesn't have str.repeat + var padding = new Array(120 + 1).join('a'); for (var i = 0; i < 10; i++) { res.write(padding); } diff --git a/test/test.js b/test/test.js index 1d98b70..1a7fe08 100644 --- a/test/test.js +++ b/test/test.js @@ -730,7 +730,8 @@ describe('node-fetch', function() { url = base + '/encoding/chunked'; return fetch(url).then(function(res) { expect(res.status).to.equal(200); - var padding = 'a'.repeat(10); + // because node v0.12 doesn't have str.repeat + var padding = new Array(10 + 1).join('a'); return res.text().then(function(result) { expect(result).to.equal(padding + '
日本語
'); }); @@ -741,7 +742,8 @@ describe('node-fetch', function() { url = base + '/encoding/invalid'; return fetch(url).then(function(res) { expect(res.status).to.equal(200); - var padding = 'a'.repeat(1200); + // because node v0.12 doesn't have str.repeat + var padding = new Array(1200 + 1).join('a'); return res.text().then(function(result) { expect(result).to.not.equal(padding + '中文'); }); From 42d822273f0eff6309607d94d16f32fa52481c02 Mon Sep 17 00:00:00 2001 From: David Frank Date: Sat, 19 Mar 2016 18:41:11 +0800 Subject: [PATCH 13/16] node 0.10 need the status code fallback --- lib/response.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/response.js b/lib/response.js index adae440..8313379 100644 --- a/lib/response.js +++ b/lib/response.js @@ -22,7 +22,7 @@ function Response(body, opts) { this.url = opts.url; this.status = opts.status; - this.statusText = opts.statusText; + this.statusText = opts.statusText || http.STATUS_CODES[this.status]; this.headers = new Headers(opts.headers); this.ok = this.status >= 200 && this.status < 300; From 839354b76b80a92ebb9e042ad928853906395fea Mon Sep 17 00:00:00 2001 From: David Frank Date: Sat, 19 Mar 2016 18:45:02 +0800 Subject: [PATCH 14/16] fix missing require --- lib/response.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/response.js b/lib/response.js index 8313379..62c353d 100644 --- a/lib/response.js +++ b/lib/response.js @@ -4,6 +4,7 @@ * Response class provides content decoding */ +var http = require('http'); var Headers = require('./headers'); var Body = require('./body'); From 4b589bc1476e60aa4ccbe5a0d597ff00b0b319bf Mon Sep 17 00:00:00 2001 From: David Frank Date: Sat, 19 Mar 2016 18:52:13 +0800 Subject: [PATCH 15/16] node v0.10 allow header support is weird, attempt to fix test --- test/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/server.js b/test/server.js index 262b638..89732ec 100644 --- a/test/server.js +++ b/test/server.js @@ -43,7 +43,7 @@ TestServer.prototype.router = function(req, res) { if (p === '/options') { res.statusCode = 200; - res.setHeader('Allow', ['GET', 'HEAD', 'OPTIONS']); + res.setHeader('Allow', 'GET, HEAD, OPTIONS'); res.end('hello world'); } From 14c20dd22a1193b4c80c7390303136c4515a96ab Mon Sep 17 00:00:00 2001 From: David Frank Date: Sat, 19 Mar 2016 18:57:41 +0800 Subject: [PATCH 16/16] changelog update --- CHANGELOG.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 337bd7b..dc12bee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,17 @@ Changelog # 1.x release -## v1.3.3 (master) +## v1.4.0 (master) + +- Enhance: Request and Response now have `clone` method (thx to @kirill-konshin for the initial PR) +- Enhance: Request and Response now have proper string and buffer body support (thx to @kirill-konshin) +- Enhance: Body constructor has been refactored out (thx to @kirill-konshin) +- Enhance: Headers now has `forEach` method (thx to @tricoder42) +- Enhance: back to 100% code coverage +- Fix: better form-data support (thx to @item4) +- Fix: better character encoding detection under chunked encoding (thx to @dsuket for the initial PR) + +## v1.3.3 - Fix: make sure `Content-Length` header is set when body is string for POST/PUT/PATCH requests - Fix: handle body stream error, for cases such as incorrect `Content-Encoding` header