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]
|
||||
[![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
|
||||
|
|
71
index.js
71
index.js
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
16
test/test.js
16
test/test.js
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue