From 362aa087caf1bdcb6eecce3d7b8faf494c74b1f6 Mon Sep 17 00:00:00 2001 From: David Frank Date: Sat, 19 Mar 2016 18:06:33 +0800 Subject: [PATCH] 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');