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