basic fetch feature done

This commit is contained in:
David Frank 2015-01-27 01:46:32 +08:00
parent f57ebe10df
commit 93a983d815
5 changed files with 102 additions and 27 deletions

View File

@ -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.

View File

@ -5,20 +5,20 @@ node-fetch
[![npm version][npm-image]][npm-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
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
@ -28,12 +28,11 @@ Hence `node-fetch`.
- 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.
- See [LIMITS.md](https://github.com/bitinn/node-fetch/blob/master/LIMITS.md)
- This module is WIP, see [Known limits](https://github.com/bitinn/node-fetch/blob/master/LIMITS.md) for details.
(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
@ -53,7 +52,7 @@ MIT
# 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

View File

@ -10,7 +10,7 @@ var resolve = require('url').resolve;
var http = require('http');
var https = require('https');
var zlib = require('zlib');
var PassThrough = require('stream').PassThrough;
var stream = require('stream');
module.exports = Fetch;
@ -30,21 +30,25 @@ function Fetch(url, opts) {
throw new Error('native promise missing, set Fetch.Promise to your favorite alternative');
}
var self = this;
return new Fetch.Promise(function(resolve, reject) {
opts = opts || {};
var uri = parse(url);
if (!uri.protocol || !uri.hostname) {
reject(Error('only absolute url are supported'));
reject(Error('only absolute urls are supported'));
return;
}
if (uri.protocol !== 'http:' && uri.protocol !== 'https:') {
reject(Error('only http(s) protocol are supported'));
reject(Error('only http(s) protocols are supported'));
return;
}
// TODO: detect type and decode data
var request;
if (uri.protocol === 'https:') {
request = https.request;
@ -58,35 +62,72 @@ function Fetch(url, opts) {
, port: uri.port
, method: opts.method
, path: uri.path
, headers: opts.headers
, headers: opts.headers || {}
, 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 output;
req.on('error', function(err) {
// TODO: handle network error
console.log(err.stack);
reject(new Error('request to ' + uri.href + ' failed, reason: ' + err.message));
});
req.on('response', function(res) {
output = {
headers: res.headers
, status: res.statusCode
, body: res.pipe(new PassThrough())
if (self.isRedirect(res.statusCode)) {
if (options.counter >= options.follow) {
reject(Error('maximum redirect reached at: ' + uri.href));
}
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);
});
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
Fetch.Promise = global.Promise;

View File

@ -7,7 +7,7 @@ module.exports = TestServer;
function TestServer() {
this.server = http.createServer(this.router);
this.port = 30001;
this.hostname = '127.0.0.1';
this.hostname = 'localhost';
this.server.on('error', function(err) {
console.log(err.stack);
});

View File

@ -43,6 +43,16 @@ describe('Fetch', function() {
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() {
url = '//example.com/';
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);
});
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() {
url = base + '/hello';
return fetch(url).then(function(res) {
expect(res.status).to.equal(200);
expect(res.headers).to.include({ 'content-type': 'text/plain' });
expect(res.body).to.be.an.instanceof(stream.Transform);
expect(res.url).to.equal(url);
});
});