272 lines
7.9 KiB
JavaScript
272 lines
7.9 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 Body = require('./lib/body');
|
|
var Response = require('./lib/response');
|
|
var Headers = require('./lib/headers');
|
|
var Request = require('./lib/request');
|
|
var FetchError = require('./lib/fetch-error');
|
|
|
|
// commonjs
|
|
module.exports = Fetch;
|
|
// es6 default export compatibility
|
|
module.exports.default = module.exports;
|
|
|
|
/**
|
|
* 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');
|
|
}
|
|
|
|
Body.Promise = Fetch.Promise;
|
|
|
|
var self = this;
|
|
|
|
// wrap http.request into fetch
|
|
return new Fetch.Promise(function(resolve, reject) {
|
|
// build request object
|
|
var options = new Request(url, opts);
|
|
|
|
if (!options.protocol || !options.hostname) {
|
|
throw new Error('only absolute urls are supported');
|
|
}
|
|
|
|
if (options.protocol !== 'http:' && options.protocol !== 'https:') {
|
|
throw new Error('only http(s) protocols are supported');
|
|
}
|
|
|
|
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') && !options.agent) {
|
|
headers.set('connection', 'close');
|
|
}
|
|
|
|
if (!headers.has('accept')) {
|
|
headers.set('accept', '*/*');
|
|
}
|
|
|
|
// detect form data input from form-data module, this hack avoid the need to pass multipart header manually
|
|
if (!headers.has('content-type') && options.body && typeof options.body.getBoundary === 'function') {
|
|
headers.set('content-type', 'multipart/form-data; boundary=' + options.body.getBoundary());
|
|
}
|
|
|
|
// bring node-fetch closer to browser behavior by setting content-length automatically
|
|
if (!headers.has('content-length') && /post|put|patch|delete/i.test(options.method)) {
|
|
if (typeof options.body === 'string') {
|
|
headers.set('content-length', Buffer.byteLength(options.body));
|
|
// detect form data input from form-data module, this hack avoid the need to add content-length header manually
|
|
} else if (options.body && typeof options.body.getLengthSync === 'function') {
|
|
// for form-data 1.x
|
|
if (options.body._lengthRetrievers && options.body._lengthRetrievers.length == 0) {
|
|
headers.set('content-length', options.body.getLengthSync().toString());
|
|
// for form-data 2.x
|
|
} else if (options.body.hasKnownLength && options.body.hasKnownLength()) {
|
|
headers.set('content-length', options.body.getLengthSync().toString());
|
|
}
|
|
// this is only necessary for older nodejs releases (before iojs merge)
|
|
} else if (options.body === undefined || options.body === null) {
|
|
headers.set('content-length', '0');
|
|
}
|
|
}
|
|
|
|
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 FetchError('network timeout at: ' + options.url, 'request-timeout'));
|
|
}, options.timeout);
|
|
});
|
|
}
|
|
|
|
req.on('error', function(err) {
|
|
clearTimeout(reqTimeout);
|
|
reject(new FetchError('request to ' + options.url + ' failed, reason: ' + err.message, 'system', err));
|
|
});
|
|
|
|
req.on('response', function(res) {
|
|
clearTimeout(reqTimeout);
|
|
|
|
// handle redirect
|
|
if (self.isRedirect(res.statusCode) && options.redirect !== 'manual') {
|
|
if (options.redirect === 'error') {
|
|
reject(new FetchError('redirect mode is set to error: ' + options.url, 'no-redirect'));
|
|
return;
|
|
}
|
|
|
|
if (options.counter >= options.follow) {
|
|
reject(new FetchError('maximum redirect reached at: ' + options.url, 'max-redirect'));
|
|
return;
|
|
}
|
|
|
|
if (!res.headers.location) {
|
|
reject(new FetchError('redirect location header missing at: ' + options.url, 'invalid-redirect'));
|
|
return;
|
|
}
|
|
|
|
// per fetch spec, for POST request with 301/302 response, or any request with 303 response, use GET when following redirect
|
|
if (res.statusCode === 303
|
|
|| ((res.statusCode === 301 || res.statusCode === 302) && options.method === 'POST'))
|
|
{
|
|
options.method = 'GET';
|
|
delete options.body;
|
|
delete options.headers['content-length'];
|
|
}
|
|
|
|
options.counter++;
|
|
|
|
resolve(Fetch(resolve_url(options.url, res.headers.location), options));
|
|
return;
|
|
}
|
|
|
|
// normalize location header for manual redirect mode
|
|
var headers = new Headers(res.headers);
|
|
if (options.redirect === 'manual' && headers.has('location')) {
|
|
headers.set('location', resolve_url(options.url, headers.get('location')));
|
|
}
|
|
|
|
// prepare response
|
|
var body = res.pipe(new stream.PassThrough());
|
|
var response_options = {
|
|
url: options.url
|
|
, status: res.statusCode
|
|
, statusText: res.statusMessage
|
|
, headers: headers
|
|
, size: options.size
|
|
, timeout: options.timeout
|
|
};
|
|
|
|
// response object
|
|
var output;
|
|
|
|
// in following scenarios we ignore compression support
|
|
// 1. compression support is disabled
|
|
// 2. HEAD request
|
|
// 3. no content-encoding header
|
|
// 4. no content response (204)
|
|
// 5. content not modified response (304)
|
|
if (!options.compress || options.method === 'HEAD' || !headers.has('content-encoding') || res.statusCode === 204 || res.statusCode === 304) {
|
|
output = new Response(body, response_options);
|
|
resolve(output);
|
|
return;
|
|
}
|
|
|
|
// otherwise, check for gzip or deflate
|
|
var name = headers.get('content-encoding');
|
|
|
|
// for gzip
|
|
if (name == 'gzip' || name == 'x-gzip') {
|
|
body = body.pipe(zlib.createGunzip());
|
|
output = new Response(body, response_options);
|
|
resolve(output);
|
|
return;
|
|
|
|
// for deflate
|
|
} else if (name == 'deflate' || name == 'x-deflate') {
|
|
// handle the infamous raw deflate response from old servers
|
|
// a hack for old IIS and Apache servers
|
|
var raw = res.pipe(new stream.PassThrough());
|
|
raw.once('data', function(chunk) {
|
|
// see http://stackoverflow.com/questions/37519828
|
|
if ((chunk[0] & 0x0F) === 0x08) {
|
|
body = body.pipe(zlib.createInflate());
|
|
} else {
|
|
body = body.pipe(zlib.createInflateRaw());
|
|
}
|
|
output = new Response(body, response_options);
|
|
resolve(output);
|
|
});
|
|
return;
|
|
}
|
|
|
|
// otherwise, use response as-is
|
|
output = new Response(body, response_options);
|
|
resolve(output);
|
|
return;
|
|
});
|
|
|
|
// accept string, buffer or readable stream as body
|
|
// per spec we will call tostring on non-stream objects
|
|
if (typeof options.body === 'string') {
|
|
req.write(options.body);
|
|
req.end();
|
|
} else if (options.body instanceof Buffer) {
|
|
req.write(options.body);
|
|
req.end();
|
|
} else if (typeof options.body === 'object' && options.body.pipe) {
|
|
options.body.pipe(req);
|
|
} else if (typeof options.body === 'object') {
|
|
req.write(options.body.toString());
|
|
req.end();
|
|
} 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;
|