node-fetch/index.js

190 lines
4.2 KiB
JavaScript
Raw Normal View History

2015-01-26 01:02:34 -08:00
/**
* index.js
*
2015-01-27 05:11:26 -08:00
* a request API compatible with window.fetch
2015-01-26 01:02:34 -08:00
*/
2015-01-27 07:33:06 -08:00
var parse_url = require('url').parse;
var resolve_url = require('url').resolve;
2015-01-26 01:02:34 -08:00
var http = require('http');
var https = require('https');
var zlib = require('zlib');
2015-01-26 09:46:32 -08:00
var stream = require('stream');
2015-01-26 01:02:34 -08:00
2015-01-27 05:11:26 -08:00
var Response = require('./lib/response');
var Headers = require('./lib/headers');
2015-01-26 01:02:34 -08:00
module.exports = Fetch;
/**
2015-01-27 05:11:26 -08:00
* Fetch class
2015-01-26 01:02:34 -08:00
*
* @param String url Absolute url
* @param Object opts Fetch options
* @return Promise
*/
function Fetch(url, opts) {
2015-01-27 05:11:26 -08:00
// allow call as function
2015-01-26 01:02:34 -08:00
if (!(this instanceof Fetch))
return new Fetch(url, opts);
2015-01-27 05:11:26 -08:00
// allow custom promise
2015-01-26 02:15:07 -08:00
if (!Fetch.Promise) {
2015-01-26 05:58:52 -08:00
throw new Error('native promise missing, set Fetch.Promise to your favorite alternative');
2015-01-26 02:15:07 -08:00
}
2015-01-26 01:02:34 -08:00
2015-01-27 05:11:26 -08:00
Response.Promise = Fetch.Promise;
2015-01-26 09:46:32 -08:00
var self = this;
2015-01-27 05:11:26 -08:00
// wrap http.request into fetch
2015-01-26 02:15:07 -08:00
return new Fetch.Promise(function(resolve, reject) {
2015-01-27 07:33:06 -08:00
var uri = parse_url(url);
2015-01-26 02:15:07 -08:00
if (!uri.protocol || !uri.hostname) {
2015-01-27 07:33:06 -08:00
reject(new Error('only absolute urls are supported'));
2015-01-26 02:15:07 -08:00
return;
}
if (uri.protocol !== 'http:' && uri.protocol !== 'https:') {
2015-01-27 07:33:06 -08:00
reject(new Error('only http(s) protocols are supported'));
2015-01-26 02:15:07 -08:00
return;
}
var request;
2015-01-26 05:28:23 -08:00
if (uri.protocol === 'https:') {
2015-01-26 02:15:07 -08:00
request = https.request;
} else {
request = http.request;
}
2015-01-27 05:11:26 -08:00
opts = opts || {};
// avoid side-effect on input options
2015-01-26 02:15:07 -08:00
var options = {
hostname: uri.hostname
, port: uri.port
2015-01-27 09:00:53 -08:00
, path: uri.path
2015-01-26 02:15:07 -08:00
, auth: uri.auth
2015-01-27 05:11:26 -08:00
, method: opts.method || 'GET'
, headers: opts.headers || {}
2015-01-26 09:46:32 -08:00
, follow: opts.follow || 20
, counter: opts.counter || 0
2015-01-27 05:11:26 -08:00
, timeout: opts.timeout || 0
2015-01-27 07:33:06 -08:00
, compress: opts.compress !== false
2015-01-27 05:11:26 -08:00
, size: opts.size || 0
2015-01-26 09:46:32 -08:00
, body: opts.body
2015-01-27 05:11:26 -08:00
, agent: opts.agent
2015-01-26 02:15:07 -08:00
};
2015-01-27 05:11:26 -08:00
// normalize headers
var headers = new Headers(options.headers);
if (options.compress) {
headers.set('accept-encoding', 'gzip,deflate');
}
if (!headers.has('user-agent')) {
headers.set('user-agent', 'node-fetch/1.0 (+https://github.com/bitinn/node-fetch)');
}
if (!headers.has('connection')) {
headers.set('connection', 'close');
}
if (!headers.has('accept')) {
headers.set('accept', '*/*');
}
options.headers = headers.raw();
// send request
2015-01-26 02:15:07 -08:00
var req = request(options);
2015-01-27 05:11:26 -08:00
var started = false;
req.on('socket', function(socket) {
if (!started && options.timeout) {
started = true;
setTimeout(function() {
req.abort();
reject(new Error('network timeout at: ' + uri.href));
}, options.timeout);
}
});
2015-01-26 02:15:07 -08:00
2015-01-26 05:28:23 -08:00
req.on('error', function(err) {
2015-01-26 09:46:32 -08:00
reject(new Error('request to ' + uri.href + ' failed, reason: ' + err.message));
2015-01-26 05:28:23 -08:00
});
2015-01-26 02:15:07 -08:00
req.on('response', function(res) {
2015-01-27 05:11:26 -08:00
// handle redirect
2015-01-26 09:46:32 -08:00
if (self.isRedirect(res.statusCode)) {
if (options.counter >= options.follow) {
2015-01-27 07:33:06 -08:00
reject(new Error('maximum redirect reached at: ' + uri.href));
return;
2015-01-26 09:46:32 -08:00
}
if (!res.headers.location) {
2015-01-27 07:33:06 -08:00
reject(new Error('redirect location header missing at: ' + uri.href));
return;
2015-01-26 09:46:32 -08:00
}
2015-01-27 07:33:06 -08:00
options.counter++;
resolve(Fetch(resolve_url(uri.href, res.headers.location), options));
2015-01-27 05:11:26 -08:00
return;
2015-01-26 09:46:32 -08:00
}
2015-01-27 05:11:26 -08:00
// handle compression
var body = res.pipe(new stream.PassThrough());
var headers = new Headers(res.headers);
2015-01-27 07:33:06 -08:00
if (options.compress && headers.has('content-encoding')) {
2015-01-27 05:11:26 -08:00
var name = headers.get('content-encoding');
if (name == 'gzip' || name == 'x-gzip') {
body = body.pipe(zlib.createGunzip());
} else if (name == 'deflate' || name == 'x-deflate') {
body = body.pipe(zlib.createInflate());
}
}
// response object
var output = new Response(body, {
url: uri.href
, status: res.statusCode
, headers: headers
2015-01-27 09:00:53 -08:00
, size: options.size
2015-01-27 05:11:26 -08:00
});
2015-01-26 02:15:07 -08:00
2015-01-26 05:28:23 -08:00
resolve(output);
2015-01-26 02:15:07 -08:00
});
2015-01-27 05:11:26 -08:00
// accept string or readable stream as body
2015-01-26 09:46:32 -08:00
if (typeof options.body === 'string') {
req.write(options.body);
req.end();
2015-01-27 07:33:06 -08:00
} else if (typeof options.body === 'object' && options.body.pipe) {
2015-01-26 09:46:32 -08:00
options.body.pipe(req);
} else {
req.end();
}
2015-01-26 02:15:07 -08:00
});
2015-01-26 01:02:34 -08:00
};
2015-01-26 09:46:32 -08:00
/**
2015-01-27 05:11:26 -08:00
* Redirect code matching
2015-01-26 09:46:32 -08:00
*
* @param Number code Status code
* @return Boolean
*/
Fetch.prototype.isRedirect = function(code) {
return code === 301 || code === 302 || code === 303 || code === 307 || code === 308;
}
2015-01-26 02:15:07 -08:00
// expose Promise
2015-01-26 01:02:34 -08:00
Fetch.Promise = global.Promise;