import * as capability from './capability'; import {inherits} from 'util'; import {IncomingMessage, readyStates as rStates} from './response'; import {Writable} from 'stream'; import toArrayBuffer from './to-arraybuffer'; function decideMode(preferBinary, useFetch) { if (capability.hasFetch && 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' } } export default ClientRequest; function ClientRequest(opts) { var self = this 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') { // If the use of XHR should be preferred and includes preserving the 'content-type' header 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.on('finish', function() { self._onFinish() }) } inherits(ClientRequest, Writable) // 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' ] 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 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 if (self._destroyed) return var opts = self._opts var headersObj = self._headers var body if (opts.method === 'POST' || opts.method === 'PUT' || opts.method === 'PATCH') { 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() } } if (self._mode === 'fetch') { var headers = Object.keys(headersObj).map(function(name) { return [headersObj[name].name, headersObj[name].value] }) global.fetch(self._opts.url, { method: self._opts.method, headers: headers, body: body, mode: 'cors', credentials: opts.withCredentials ? 'include' : 'same-origin' }).then(function(response) { self._fetchResponse = response self._connect() }, function(reason) { 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') Object.keys(headersObj).forEach(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() { 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.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 if (self._response) self._response._destroyed = true if (self._xhr) self._xhr.abort() // Currently, there isn't a way to truly abort a fetch. // If you like bikeshedding, see https://github.com/whatwg/fetch/issues/27 } ClientRequest.prototype.end = function(data, encoding, cb) { var self = this if (typeof data === 'function') { cb = data data = undefined } Writable.prototype.end.call(self, data, encoding, cb) } ClientRequest.prototype.flushHeaders = function() {} ClientRequest.prototype.setTimeout = function() {} ClientRequest.prototype.setNoDelay = function() {} ClientRequest.prototype.setSocketKeepAlive = function() {}