/** * 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; 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') && !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' && options.body._lengthRetrievers.length == 0) { 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; } // 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'); // no need to pipe no content and not modified response body if (res.statusCode !== 204 && res.statusCode !== 304) { if (name == 'gzip' || name == 'x-gzip') { body = body.pipe(zlib.createGunzip()); } else if (name == 'deflate' || name == 'x-deflate') { body = body.pipe(zlib.createInflate()); } } } // normalize location header for manual redirect mode if (options.redirect === 'manual' && headers.has('location')) { headers.set('location', resolve_url(options.url, headers.get('location'))); } // response object var output = new Response(body, { url: options.url , status: res.statusCode , statusText: res.statusMessage , 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;