// var Base64 = require('Base64') var capability = require('./capability') var foreach = require('foreach') var keys = require('object-keys') var response = require('./response') var stream = require('stream') var inherits = require('inherits') var IncomingMessage = response.IncomingMessage var rStates = response.readyStates function decideMode (preferBinary) { if (capability.fetch) { 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._url = opts.protocol + '//' + opts.hostname + ':' + opts.port + opts.path self._body = [] self._headers = {} if (opts.auth) self.setHeader('Authorization', 'Basic ' + new Buffer(opts.auth).toString('base64')) foreach(keys(opts.headers), function (name) { self.setHeader(name, opts.headers[name]) }) var preferBinary if (opts.mode === 'prefer-streaming') { // If streaming is a high priority but binary compatibility isn't preferBinary = false } else if (opts.mode === 'prefer-fast') { // If binary is preferred for speed preferBinary = true } else if (!opts.mode || opts.mode === 'default') { // By default, use binary if text streaming may corrupt data preferBinary = !capability.overrideMimeType } else { throw new Error('Invalid value for opts.mode') } self._mode = decideMode(preferBinary) self.on('finish', function () { self._onFinish() }) } inherits(ClientRequest, stream.Writable) ClientRequest.prototype.setHeader = function (name, value) { var self = this self._headers[name.toLowerCase()] = { name: name, value: value } } ClientRequest.prototype.getHeader = function (name) { var self = this return self._headers[name.toLowerCase()].value } ClientRequest.prototype.removeHeader = function (name) { var self = this delete self._headers[name.toLowerCase()] } ClientRequest.prototype._onFinish = function () { var self = this var opts = self._opts var headersObj = self._headers var body if (opts.method === 'POST' || opts.method === 'PUT') { if (capability.blobConstructor) { body = new window.Blob(self._body.map(function (buffer) { return buffer.toArrayBuffer() }), { type: (headersObj['content-type'] || {}).value || '' }) } else { // get utf8 string body = Buffer.concat(self._body).toString() } } if (self._mode === 'fetch') { var headers = keys(headersObj).map(function (name) { return [headersObj[name].name, headersObj[name].value] }) window.fetch(self._url, { method: self._opts.method, headers: headers, body: body, mode: 'cors', credentials: opts.withCredentials ? 'include' : 'omit' }).then(function (response) { self._fetchResponse = response self._connect() }).then(undefined, function (reason) { self.emit('error', reason) }) } else { var xhr = self._xhr = new window.XMLHttpRequest() try { xhr.open(self._opts.method, self._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') foreach(keys(headersObj), function (name) { xhr.setRequestHeader(headersObj[name].name, headersObj[name].value) }) 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 () { 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. Even though the spec says it should * be available in readyState 3, accessing it throws an exception in IE8 */ function statusValid (xhr) { try { return (xhr.status !== null) } catch (e) { return false } } ClientRequest.prototype._onXHRProgress = function () { var self = this if (!statusValid(self._xhr) || self._failed) return if (!self._response) self._connect() self._response._onXHRProgress() } ClientRequest.prototype._connect = function () { var self = this self._response = new IncomingMessage(self._xhr, self._fetchResponse, self._mode) self.emit('response', self._response) } ClientRequest.prototype._write = function (chunk, encoding, cb) { var self = this self._body.push(chunk) cb() } ClientRequest.prototype.abort = function () { var self = this if (self._xhr) self._xhr.abort() } ClientRequest.prototype.end = function (data, encoding, cb) { var self = this if (typeof data === 'function') { cb = data data = undefined } if (data) stream.Writable.push.call(self, data, encoding) stream.Writable.prototype.end.call(self, cb) } ClientRequest.prototype.flushHeaders = function () {} ClientRequest.prototype.setTimeout = function () {} ClientRequest.prototype.setNoDelay = function () {} ClientRequest.prototype.setSocketKeepAlive = function () {}