Merge pull request #26 from bitinn/refactor-request

Refactor request
This commit is contained in:
David Frank 2015-06-04 12:42:01 +08:00
commit 3b3a96d9e4
5 changed files with 141 additions and 46 deletions

View File

@ -5,7 +5,11 @@ Changelog
# 1.x release
## v1.2.1 (master)
## v1.3.0 (master)
- Enhance: now fetch.Request is exposed as well.
## v1.2.1
- Enhance: `Headers` now normalized `Number` value to `String`, prevent common mistakes

View File

@ -16,6 +16,4 @@ Known differences
- Only support `res.text()` and `res.json()` at the moment, until there are good use-cases for blob.
- Only expose `Response` and `Headers` constructors at the moment, we don't see a good use-case for `Request` interface yet.
- There is currently no built-in caching, as server-side caching varies by use-cases.

View File

@ -14,13 +14,14 @@ var stream = require('stream');
var Response = require('./lib/response');
var Headers = require('./lib/headers');
var Request = require('./lib/request');
module.exports = Fetch;
/**
* Fetch class
*
* @param String url Absolute url
* @param Mixed url Absolute url or Request instance
* @param Object opts Fetch options
* @return Promise
*/
@ -41,44 +42,22 @@ function Fetch(url, opts) {
// wrap http.request into fetch
return new Fetch.Promise(function(resolve, reject) {
var uri = parse_url(url);
if (!uri.protocol || !uri.hostname) {
reject(new Error('only absolute urls are supported'));
// build request object
var options;
try {
options = new Request(url, opts);
} catch (err) {
reject(err);
return;
}
if (uri.protocol !== 'http:' && uri.protocol !== 'https:') {
reject(new Error('only http(s) protocols are supported'));
return;
}
var request;
if (uri.protocol === 'https:') {
request = https.request;
var send;
if (options.protocol === 'https:') {
send = https.request;
} else {
request = http.request;
send = http.request;
}
opts = opts || {};
// avoid side-effect on input options
var options = {
hostname: uri.hostname
, port: uri.port
, path: uri.path
, auth: uri.auth
, method: opts.method || 'GET'
, headers: opts.headers || {}
, follow: opts.follow !== undefined ? opts.follow : 20
, counter: opts.counter || 0
, timeout: opts.timeout || 0
, compress: opts.compress !== false
, size: opts.size || 0
, body: opts.body
, agent: opts.agent
};
// normalize headers
var headers = new Headers(options.headers);
@ -101,21 +80,21 @@ function Fetch(url, opts) {
options.headers = headers.raw();
// send request
var req = request(options);
var req = send(options);
var reqTimeout;
if (options.timeout) {
req.once('socket', function(socket) {
reqTimeout = setTimeout(function() {
req.abort();
reject(new Error('network timeout at: ' + uri.href));
reject(new Error('network timeout at: ' + options.url));
}, options.timeout);
});
}
req.on('error', function(err) {
clearTimeout(reqTimeout);
reject(new Error('request to ' + uri.href + ' failed, reason: ' + err.message));
reject(new Error('request to ' + options.url + ' failed, reason: ' + err.message));
});
req.on('response', function(res) {
@ -124,18 +103,18 @@ function Fetch(url, opts) {
// handle redirect
if (self.isRedirect(res.statusCode)) {
if (options.counter >= options.follow) {
reject(new Error('maximum redirect reached at: ' + uri.href));
reject(new Error('maximum redirect reached at: ' + options.url));
return;
}
if (!res.headers.location) {
reject(new Error('redirect location header missing at: ' + uri.href));
reject(new Error('redirect location header missing at: ' + options.url));
return;
}
options.counter++;
resolve(Fetch(resolve_url(uri.href, res.headers.location), options));
resolve(Fetch(resolve_url(options.url, res.headers.location), options));
return;
}
@ -155,7 +134,7 @@ function Fetch(url, opts) {
// response object
var output = new Response(body, {
url: uri.href
url: options.url
, status: res.statusCode
, headers: headers
, size: options.size
@ -192,3 +171,4 @@ Fetch.prototype.isRedirect = function(code) {
Fetch.Promise = global.Promise;
Fetch.Response = Response;
Fetch.Headers = Headers;
Fetch.Request = Request;

67
lib/request.js Normal file
View File

@ -0,0 +1,67 @@
/**
* request.js
*
* Request class contains server only options
*/
var parse_url = require('url').parse;
module.exports = Request;
/**
* Request class
*
* @param Mixed input Url or Request instance
* @param Object init Custom options
* @return Void
*/
function Request(input, init) {
var url, url_parsed;
// normalize input
if (!(input instanceof Request)) {
url = input;
url_parsed = parse_url(url);
input = {};
} else {
url = input.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
init = init || {};
// fetch spec options
this.method = init.method || input.method || 'GET';
this.headers = init.headers || input.headers || {};
this.body = init.body || input.body;
this.url = url;
// server only options
this.follow = init.follow !== undefined ?
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;
// server request options
this.protocol = url_parsed.protocol;
this.hostname = url_parsed.hostname;
this.port = url_parsed.port;
this.path = url_parsed.path;
this.auth = url_parsed.auth;
}

View File

@ -16,6 +16,7 @@ var TestServer = require('./server');
var fetch = require('../index.js');
var Headers = require('../lib/headers.js');
var Response = require('../lib/response.js');
var Request = require('../lib/request.js');
// test with native promise on node 0.11, and bluebird for node 0.10
fetch.Promise = fetch.Promise || bluebird;
@ -59,9 +60,10 @@ describe('node-fetch', function() {
fetch.Promise = old;
});
it('should expose Headers and Response constructors', function() {
it('should expose Headers, Response and Request constructors', function() {
expect(fetch.Headers).to.equal(Headers);
expect(fetch.Response).to.equal(Response);
expect(fetch.Request).to.equal(Request);
});
it('should reject with error if url is protocol relative', function() {
@ -474,7 +476,6 @@ describe('node-fetch', function() {
url = base + '/hello';
opts = {
method: 'HEAD'
};
return fetch(url, opts).then(function(res) {
expect(res.status).to.equal(200);
@ -693,6 +694,52 @@ describe('node-fetch', function() {
expect(h3._headers['b']).to.include('1');
});
it('should support fetch with Request instance', function() {
url = base + '/hello';
var req = new Request(url);
return fetch(req).then(function(res) {
expect(res.url).to.equal(url);
expect(res.ok).to.be.true;
expect(res.status).to.equal(200);
});
});
it('should support wrapping Request instance', function() {
url = base + '/hello';
var r1 = new Request(url, {
method: 'POST'
, follow: 1
});
var r2 = new Request(r1, {
follow: 2
})
expect(r2.url).to.equal(url);
expect(r2.method).to.equal('POST');
expect(r1.follow).to.equal(1);
expect(r2.follow).to.equal(2);
});
it('should support overwrite Request instance', function() {
url = base + '/inspect';
var req = new Request(url, {
method: 'POST'
, headers: {
a: '1'
}
});
return fetch(req, {
method: 'GET'
, headers: {
a: '2'
}
}).then(function(res) {
return res.json();
}).then(function(body) {
expect(body.method).to.equal('GET');
expect(body.headers.a).to.equal('2');
});
});
it('should support https request', function() {
this.timeout(5000);
url = 'https://github.com/';
@ -704,5 +751,4 @@ describe('node-fetch', function() {
expect(res.ok).to.be.true;
});
});
});