| 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' | |
| ]
 |