basic fetch feature done
This commit is contained in:
parent
f57ebe10df
commit
93a983d815
19
LIMITS.md
19
LIMITS.md
|
@ -0,0 +1,19 @@
|
||||||
|
|
||||||
|
Known limits
|
||||||
|
============
|
||||||
|
|
||||||
|
**As of 1.x release**
|
||||||
|
|
||||||
|
- Topics such as cross-origin, CSP, mixed content are ignored, given our server-side context.
|
||||||
|
|
||||||
|
- Url input must be an absolute url, using either `http` or `https` as scheme.
|
||||||
|
|
||||||
|
- Doesn't export `Headers`, `Body`, `Request`, `Response` classes yet, as we currenly use a much simpler implementation.
|
||||||
|
|
||||||
|
- For convenience, `res.body()` is a transform stream instead of byte stream, so decoding can be handled independently.
|
||||||
|
|
||||||
|
- Similarly, `options.body` can either be a string or a readable stream.
|
||||||
|
|
||||||
|
- For convenience, maximum redirect count (`options.follow`) and request timeout (`options.timeout`) are adjustable.
|
||||||
|
|
||||||
|
- There is currently no built-in caching support, as server-side requirement varies greatly between use-cases.
|
21
README.md
21
README.md
|
@ -5,20 +5,20 @@ node-fetch
|
||||||
[![npm version][npm-image]][npm-url]
|
[![npm version][npm-image]][npm-url]
|
||||||
[![build status][travis-image]][travis-url]
|
[![build status][travis-image]][travis-url]
|
||||||
|
|
||||||
A light-weight module that brings window.fetch to node.js
|
A light-weight module that brings `window.fetch` to node.js
|
||||||
|
|
||||||
|
|
||||||
# Motivation
|
# Motivation
|
||||||
|
|
||||||
I like the notion of Matt Andrews' [isomorphic-fetch](https://github.com/matthew-andrews/isomorphic-fetch): it bridges the API gap between client-side and server-side http requests, so developers have less to worry about.
|
I really like the notion of Matt Andrews' [isomorphic-fetch](https://github.com/matthew-andrews/isomorphic-fetch): it bridges the API gap between client-side and server-side http requests, so developers have less to worry about.
|
||||||
|
|
||||||
But I believe the term [isomorphic](http://isomorphic.net/) is generally misleading: it gives developers a false sense of security that their javascript code will run happily on both controlled server environment as well as uncontrollable user browsers. When the latter is only true for a small subset of modern browsers, not to mention quirks in native implementation.
|
But I think the term [isomorphic](http://isomorphic.net/) is generally misleading: it gives developers a false sense of security that their javascript code will run happily on both controlled server environment as well as uncontrollable user browsers. When the latter is only true for a small subset of modern browsers, not to mention quirks in native implementation.
|
||||||
|
|
||||||
Instead of implementing `XMLHttpRequest` in node to run browser-specific [fetch polyfill](https://github.com/github/fetch), why not go from node's `http` to `fetch` API directly? Node has native stream support, your browserify build targets (browsers) don't, so underneath they are going to be vastly different anyway.
|
Instead of implementing `XMLHttpRequest` in node to run browser-specific [fetch polyfill](https://github.com/github/fetch), why not go from node's `http` to `fetch` API directly? Node has native stream support, browserify build targets (browsers) don't, so underneath they are going to be vastly different anyway.
|
||||||
|
|
||||||
IMHO, it's safer to be aware of javascript runtime's strength and weakness, than to assume they are a unified platform under a stable spec.
|
IMHO, it's safer to be aware of javascript runtime's strength and weakness, than to assume they are a unified platform under a singular spec.
|
||||||
|
|
||||||
Hence `node-fetch`.
|
Hence `node-fetch`, minimal code for a `window.fetch` compatible API.
|
||||||
|
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
|
@ -28,12 +28,11 @@ Hence `node-fetch`.
|
||||||
- Use native promise, but allow substituting it with [insert your favorite promise library].
|
- Use native promise, but allow substituting it with [insert your favorite promise library].
|
||||||
|
|
||||||
|
|
||||||
# Limits
|
# Difference to client-side fetch
|
||||||
|
|
||||||
- Work in progress, much like the spec itself.
|
- This module is WIP, see [Known limits](https://github.com/bitinn/node-fetch/blob/master/LIMITS.md) for details.
|
||||||
- See [LIMITS.md](https://github.com/bitinn/node-fetch/blob/master/LIMITS.md)
|
|
||||||
|
|
||||||
(If you spot a undocumented difference, feel free to open an issue. Pull requests are welcomed too!)
|
(If you spot a missing feature that `window.fetch` offers, feel free to open an issue. Pull requests are welcomed too!)
|
||||||
|
|
||||||
|
|
||||||
# Install
|
# Install
|
||||||
|
@ -53,7 +52,7 @@ MIT
|
||||||
|
|
||||||
# Acknowledgement
|
# Acknowledgement
|
||||||
|
|
||||||
Thanks to github/fetch for providing a solid implementation reference.
|
Thanks to [github/fetch](https://github.com/github/fetch) for providing a solid implementation reference.
|
||||||
|
|
||||||
|
|
||||||
[npm-image]: https://img.shields.io/npm/v/node-fetch.svg?style=flat-square
|
[npm-image]: https://img.shields.io/npm/v/node-fetch.svg?style=flat-square
|
||||||
|
|
71
index.js
71
index.js
|
@ -10,7 +10,7 @@ var resolve = require('url').resolve;
|
||||||
var http = require('http');
|
var http = require('http');
|
||||||
var https = require('https');
|
var https = require('https');
|
||||||
var zlib = require('zlib');
|
var zlib = require('zlib');
|
||||||
var PassThrough = require('stream').PassThrough;
|
var stream = require('stream');
|
||||||
|
|
||||||
module.exports = Fetch;
|
module.exports = Fetch;
|
||||||
|
|
||||||
|
@ -30,21 +30,25 @@ function Fetch(url, opts) {
|
||||||
throw new Error('native promise missing, set Fetch.Promise to your favorite alternative');
|
throw new Error('native promise missing, set Fetch.Promise to your favorite alternative');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
|
||||||
return new Fetch.Promise(function(resolve, reject) {
|
return new Fetch.Promise(function(resolve, reject) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
|
|
||||||
var uri = parse(url);
|
var uri = parse(url);
|
||||||
|
|
||||||
if (!uri.protocol || !uri.hostname) {
|
if (!uri.protocol || !uri.hostname) {
|
||||||
reject(Error('only absolute url are supported'));
|
reject(Error('only absolute urls are supported'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uri.protocol !== 'http:' && uri.protocol !== 'https:') {
|
if (uri.protocol !== 'http:' && uri.protocol !== 'https:') {
|
||||||
reject(Error('only http(s) protocol are supported'));
|
reject(Error('only http(s) protocols are supported'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: detect type and decode data
|
||||||
|
|
||||||
var request;
|
var request;
|
||||||
if (uri.protocol === 'https:') {
|
if (uri.protocol === 'https:') {
|
||||||
request = https.request;
|
request = https.request;
|
||||||
|
@ -58,35 +62,72 @@ function Fetch(url, opts) {
|
||||||
, port: uri.port
|
, port: uri.port
|
||||||
, method: opts.method
|
, method: opts.method
|
||||||
, path: uri.path
|
, path: uri.path
|
||||||
, headers: opts.headers
|
, headers: opts.headers || {}
|
||||||
, auth: uri.auth
|
, auth: uri.auth
|
||||||
//, agent: opts.agent
|
, follow: opts.follow || 20
|
||||||
|
, counter: opts.counter || 0
|
||||||
|
, agent: opts.agent
|
||||||
|
, body: opts.body
|
||||||
|
, timeout: opts.timeout
|
||||||
};
|
};
|
||||||
|
|
||||||
var req = request(options);
|
var req = request(options);
|
||||||
var output;
|
|
||||||
|
|
||||||
req.on('error', function(err) {
|
req.on('error', function(err) {
|
||||||
// TODO: handle network error
|
reject(new Error('request to ' + uri.href + ' failed, reason: ' + err.message));
|
||||||
console.log(err.stack);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
req.on('response', function(res) {
|
req.on('response', function(res) {
|
||||||
output = {
|
if (self.isRedirect(res.statusCode)) {
|
||||||
headers: res.headers
|
if (options.counter >= options.follow) {
|
||||||
, status: res.statusCode
|
reject(Error('maximum redirect reached at: ' + uri.href));
|
||||||
, body: res.pipe(new PassThrough())
|
}
|
||||||
|
|
||||||
|
if (!res.headers.location) {
|
||||||
|
reject(Error('redirect location header missing at: ' + uri.href));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Fetch(resolve(uri.href, res.headers.location), options);
|
||||||
|
}
|
||||||
|
|
||||||
|
var output = {
|
||||||
|
status: res.statusCode
|
||||||
|
, headers: res.headers
|
||||||
|
, body: res.pipe(new stream.PassThrough())
|
||||||
|
, url: uri.href
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: redirect
|
|
||||||
// TODO: type switch
|
|
||||||
resolve(output);
|
resolve(output);
|
||||||
});
|
});
|
||||||
|
|
||||||
req.end();
|
if (typeof options.body === 'string') {
|
||||||
|
req.write(options.body);
|
||||||
|
req.end();
|
||||||
|
} else if (options.body instanceof stream.Readable) {
|
||||||
|
options.body.pipe(req);
|
||||||
|
} else {
|
||||||
|
req.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.timeout) {
|
||||||
|
setTimeout(function() {
|
||||||
|
req.abort();
|
||||||
|
reject(new Error('network timeout at: ' + uri.href));
|
||||||
|
}, options.timeout);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an instance of Decent
|
||||||
|
*
|
||||||
|
* @param Number code Status code
|
||||||
|
* @return Boolean
|
||||||
|
*/
|
||||||
|
Fetch.prototype.isRedirect = function(code) {
|
||||||
|
return code === 301 || code === 302 || code === 303 || code === 307 || code === 308;
|
||||||
|
}
|
||||||
|
|
||||||
// expose Promise
|
// expose Promise
|
||||||
Fetch.Promise = global.Promise;
|
Fetch.Promise = global.Promise;
|
||||||
|
|
|
@ -7,7 +7,7 @@ module.exports = TestServer;
|
||||||
function TestServer() {
|
function TestServer() {
|
||||||
this.server = http.createServer(this.router);
|
this.server = http.createServer(this.router);
|
||||||
this.port = 30001;
|
this.port = 30001;
|
||||||
this.hostname = '127.0.0.1';
|
this.hostname = 'localhost';
|
||||||
this.server.on('error', function(err) {
|
this.server.on('error', function(err) {
|
||||||
console.log(err.stack);
|
console.log(err.stack);
|
||||||
});
|
});
|
||||||
|
|
16
test/test.js
16
test/test.js
|
@ -43,6 +43,16 @@ describe('Fetch', function() {
|
||||||
fetch.Promise = old;
|
fetch.Promise = old;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw error when no promise implementation found', function() {
|
||||||
|
url = 'http://example.com/';
|
||||||
|
var old = fetch.Promise;
|
||||||
|
fetch.Promise = undefined;
|
||||||
|
expect(function() {
|
||||||
|
fetch(url)
|
||||||
|
}).to.throw(Error);
|
||||||
|
fetch.Promise = old;
|
||||||
|
});
|
||||||
|
|
||||||
it('should reject with error if url is protocol relative', function() {
|
it('should reject with error if url is protocol relative', function() {
|
||||||
url = '//example.com/';
|
url = '//example.com/';
|
||||||
return expect(fetch(url)).to.eventually.be.rejectedWith(Error);
|
return expect(fetch(url)).to.eventually.be.rejectedWith(Error);
|
||||||
|
@ -58,12 +68,18 @@ describe('Fetch', function() {
|
||||||
return expect(fetch(url)).to.eventually.be.rejectedWith(Error);
|
return expect(fetch(url)).to.eventually.be.rejectedWith(Error);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should reject with error on network failure', function() {
|
||||||
|
url = 'http://localhost:50000/';
|
||||||
|
return expect(fetch(url)).to.eventually.be.rejectedWith(Error);
|
||||||
|
});
|
||||||
|
|
||||||
it('should resolve status code, headers, body correctly', function() {
|
it('should resolve status code, headers, body correctly', function() {
|
||||||
url = base + '/hello';
|
url = base + '/hello';
|
||||||
return fetch(url).then(function(res) {
|
return fetch(url).then(function(res) {
|
||||||
expect(res.status).to.equal(200);
|
expect(res.status).to.equal(200);
|
||||||
expect(res.headers).to.include({ 'content-type': 'text/plain' });
|
expect(res.headers).to.include({ 'content-type': 'text/plain' });
|
||||||
expect(res.body).to.be.an.instanceof(stream.Transform);
|
expect(res.body).to.be.an.instanceof(stream.Transform);
|
||||||
|
expect(res.url).to.equal(url);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue