var capability = require('./capability'); var inherits = require('inherits'); var response = require('./response'); var stream = require('readable-stream'); var toArrayBuffer = require('to-arraybuffer'); var IncomingMessage = response.IncomingMessage; var rStates = response.readyStates; function decideMode(preferBinary, useFetch) { if (capability.fetch && useFetch) { return 'fetch'; } else if (capability.mozchunkedarraybuffer) { return 'moz-chunked-arraybuffer'; } else if (capability.msstream) { return 'ms-stream'; } else if (capability.arraybuffer && preferBinary) { return 'arraybuffer'; } else if (capability.vbArray && preferBinary) { return 'text:vbarray'; } else { return 'text'; } } var ClientRequest = (module.exports = function (opts) { var self = this; stream.Writable.call(self); self._opts = opts; self._body = []; self._headers = {}; if (opts.auth) self.setHeader('Authorization', 'Basic ' + new Buffer(opts.auth).toString('base64')); Object.keys(opts.headers).forEach(function (name) { self.setHeader(name, opts.headers[name]); }); var preferBinary; var useFetch = true; if (opts.mode === 'disable-fetch' || ('requestTimeout' in opts && !capability.abortController)) { // If the use of XHR should be preferred. Not typically needed. useFetch = false; preferBinary = true; } else if (opts.mode === 'prefer-streaming') { // If streaming is a high priority but binary compatibility and // the accuracy of the 'content-type' header aren't preferBinary = false; } else if (opts.mode === 'allow-wrong-content-type') { // If streaming is more important than preserving the 'content-type' header preferBinary = !capability.overrideMimeType; } else if (!opts.mode || opts.mode === 'default' || opts.mode === 'prefer-fast') { // Use binary if text streaming may corrupt data or the content-type header, or for speed preferBinary = true; } else { throw new Error('Invalid value for opts.mode'); } self._mode = decideMode(preferBinary, useFetch); self._fetchTimer = null; self.on('finish', function () { self._onFinish(); }); }); inherits(ClientRequest, stream.Writable); ClientRequest.prototype.setHeader = function (name, value) { var self = this; var lowerName = name.toLowerCase(); // This check is not necessary, but it prevents warnings from browsers about setting unsafe // headers. To be honest I'm not entirely sure hiding these warnings is a good thing, but // http-browserify did it, so I will too. if (unsafeHeaders.indexOf(lowerName) !== -1) return; self._headers[lowerName] = { name: name, value: value }; }; ClientRequest.prototype.getHeader = function (name) { var header = this._headers[name.toLowerCase()]; if (header) return header.value; return null; }; ClientRequest.prototype.removeHeader = function (name) { var self = this; delete self._headers[name.toLowerCase()]; }; ClientRequest.prototype._onFinish = function () { var self = this; if (self._destroyed) return; var opts = self._opts; var headersObj = self._headers; var body = null; if (opts.method !== 'GET' && opts.method !== 'HEAD') { if (capability.arraybuffer) { body = toArrayBuffer(Buffer.concat(self._body)); } else if (capability.blobConstructor) { body = new global.Blob( self._body.map(function (buffer) { return toArrayBuffer(buffer); }), { type: (headersObj['content-type'] || {}).value || '' } ); } else { // get utf8 string body = Buffer.concat(self._body).toString(); } } // create flattened list of headers var headersList = []; Object.keys(headersObj).forEach(function (keyName) { var name = headersObj[keyName].name; var value = headersObj[keyName].value; if (Array.isArray(value)) { value.forEach(function (v) { headersList.push([name, v]); }); } else { headersList.push([name, value]); } }); if (self._mode === 'fetch') { var signal = null; var fetchTimer = null; if (capability.abortController) { var controller = new AbortController(); signal = controller.signal; self._fetchAbortController = controller; if ('requestTimeout' in opts && opts.requestTimeout !== 0) { self._fetchTimer = global.setTimeout(function () { self.emit('requestTimeout'); if (self._fetchAbortController) self._fetchAbortController.abort(); }, opts.requestTimeout); } } global .fetch(self._opts.url, { method: self._opts.method, headers: headersList, body: body || undefined, mode: 'cors', credentials: opts.withCredentials ? 'include' : 'same-origin', signal: signal }) .then( function (response) { self._fetchResponse = response; self._connect(); }, function (reason) { global.clearTimeout(self._fetchTimer); if (!self._destroyed) self.emit('error', reason); } ); } else { var xhr = (self._xhr = new global.XMLHttpRequest()); try { xhr.open(self._opts.method, self._opts.url, true); } catch (err) { process.nextTick(function () { self.emit('error', err); }); return; } // Can't set responseType on really old browsers if ('responseType' in xhr) xhr.responseType = self._mode.split(':')[0]; if ('withCredentials' in xhr) xhr.withCredentials = !!opts.withCredentials; if (self._mode === 'text' && 'overrideMimeType' in xhr) xhr.overrideMimeType('text/plain; charset=x-user-defined'); if ('requestTimeout' in opts) { xhr.timeout = opts.requestTimeout; xhr.ontimeout = function () { self.emit('requestTimeout'); }; } headersList.forEach(function (header) { xhr.setRequestHeader(header[0], header[1]); }); self._response = null; xhr.onreadystatechange = function () { switch (xhr.readyState) { case rStates.LOADING: case rStates.DONE: self._onXHRProgress(); break; } }; // Necessary for streaming in Firefox, since xhr.response is ONLY defined // in onprogress, not in onreadystatechange with xhr.readyState = 3 if (self._mode === 'moz-chunked-arraybuffer') { xhr.onprogress = function () { self._onXHRProgress(); }; } xhr.onerror = function () { if (self._destroyed) return; self.emit('error', new Error('XHR error')); }; try { xhr.send(body); } catch (err) { process.nextTick(function () { self.emit('error', err); }); return; } } }; /** * Checks if xhr.status is readable and non-zero, indicating no error. * Even though the spec says it should be available in readyState 3, * accessing it throws an exception in IE8 */ function statusValid(xhr) { try { var status = xhr.status; return status !== null && status !== 0; } catch (e) { return false; } } ClientRequest.prototype._onXHRProgress = function () { var self = this; if (!statusValid(self._xhr) || self._destroyed) return; if (!self._response) self._connect(); self._response._onXHRProgress(); }; ClientRequest.prototype._connect = function () { var self = this; if (self._destroyed) return; self._response = new IncomingMessage(self._xhr, self._fetchResponse, self._mode, self._fetchTimer); self._response.on('error', function (err) { self.emit('error', err); }); self.emit('response', self._response); }; ClientRequest.prototype._write = function (chunk, encoding, cb) { var self = this; self._body.push(chunk); cb(); }; ClientRequest.prototype.abort = ClientRequest.prototype.destroy = function () { var self = this; self._destroyed = true; global.clearTimeout(self._fetchTimer); if (self._response) self._response._destroyed = true; if (self._xhr) self._xhr.abort(); else if (self._fetchAbortController) self._fetchAbortController.abort(); }; ClientRequest.prototype.end = function (data, encoding, cb) { var self = this; if (typeof data === 'function') { cb = data; data = undefined; } stream.Writable.prototype.end.call(self, data, encoding, cb); }; ClientRequest.prototype.flushHeaders = function () {}; ClientRequest.prototype.setTimeout = function () {}; ClientRequest.prototype.setNoDelay = function () {}; ClientRequest.prototype.setSocketKeepAlive = function () {}; // Taken from http://www.w3.org/TR/XMLHttpRequest/#the-setrequestheader%28%29-method var unsafeHeaders = [ 'accept-charset', 'accept-encoding', 'access-control-request-headers', 'access-control-request-method', 'connection', 'content-length', 'cookie', 'cookie2', 'date', 'dnt', 'expect', 'host', 'keep-alive', 'origin', 'referer', 'te', 'trailer', 'transfer-encoding', 'upgrade', 'user-agent', 'via' ];