180 lines
4.0 KiB
JavaScript
180 lines
4.0 KiB
JavaScript
|
|
/**
|
|
* index.js
|
|
*
|
|
* a request API compatible with window.fetch
|
|
*/
|
|
|
|
var parse_url = require('url').parse;
|
|
var resolve_url = require('url').resolve;
|
|
var http = require('http');
|
|
var https = require('https');
|
|
var zlib = require('zlib');
|
|
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 Mixed url Absolute url or Request instance
|
|
* @param Object opts Fetch options
|
|
* @return Promise
|
|
*/
|
|
function Fetch(url, opts) {
|
|
|
|
// allow call as function
|
|
if (!(this instanceof Fetch))
|
|
return new Fetch(url, opts);
|
|
|
|
// allow custom promise
|
|
if (!Fetch.Promise) {
|
|
throw new Error('native promise missing, set Fetch.Promise to your favorite alternative');
|
|
}
|
|
|
|
Response.Promise = Fetch.Promise;
|
|
|
|
var self = this;
|
|
|
|
// wrap http.request into fetch
|
|
return new Fetch.Promise(function(resolve, reject) {
|
|
// build request object
|
|
var options;
|
|
try {
|
|
options = new Request(url, opts);
|
|
} catch (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
var send;
|
|
if (options.protocol === 'https:') {
|
|
send = https.request;
|
|
} else {
|
|
send = http.request;
|
|
}
|
|
|
|
// 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();
|
|
|
|
// http.request only support string as host header, this hack make custom host header possible
|
|
if (options.headers.host) {
|
|
options.headers.host = options.headers.host[0];
|
|
}
|
|
|
|
// send request
|
|
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: ' + options.url));
|
|
}, options.timeout);
|
|
});
|
|
}
|
|
|
|
req.on('error', function(err) {
|
|
clearTimeout(reqTimeout);
|
|
reject(new Error('request to ' + options.url + ' failed, reason: ' + err.message));
|
|
});
|
|
|
|
req.on('response', function(res) {
|
|
clearTimeout(reqTimeout);
|
|
|
|
// handle redirect
|
|
if (self.isRedirect(res.statusCode)) {
|
|
if (options.counter >= options.follow) {
|
|
reject(new Error('maximum redirect reached at: ' + options.url));
|
|
return;
|
|
}
|
|
|
|
if (!res.headers.location) {
|
|
reject(new Error('redirect location header missing at: ' + options.url));
|
|
return;
|
|
}
|
|
|
|
options.counter++;
|
|
|
|
resolve(Fetch(resolve_url(options.url, res.headers.location), options));
|
|
return;
|
|
}
|
|
|
|
// handle compression
|
|
var body = res.pipe(new stream.PassThrough());
|
|
var headers = new Headers(res.headers);
|
|
|
|
if (options.compress && headers.has('content-encoding')) {
|
|
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: options.url
|
|
, status: res.statusCode
|
|
, headers: headers
|
|
, size: options.size
|
|
, timeout: options.timeout
|
|
});
|
|
|
|
resolve(output);
|
|
});
|
|
|
|
// accept string or readable stream as body
|
|
if (typeof options.body === 'string') {
|
|
req.write(options.body);
|
|
req.end();
|
|
} else if (typeof options.body === 'object' && options.body.pipe) {
|
|
options.body.pipe(req);
|
|
} else {
|
|
req.end();
|
|
}
|
|
});
|
|
|
|
};
|
|
|
|
/**
|
|
* Redirect code matching
|
|
*
|
|
* @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;
|
|
Fetch.Response = Response;
|
|
Fetch.Headers = Headers;
|
|
Fetch.Request = Request;
|