123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271 |
- /**
- * 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;
|