/** * Module requirements. */ var XMLHttpRequest = require('xmlhttprequest'); var Polling = require('./polling'); var Emitter = require('component-emitter'); var inherit = require('component-inherit'); var debug = require('debug')('engine.io-client:polling-xhr'); /** * Module exports. */ module.exports = XHR; module.exports.Request = Request; /** * Empty function */ function empty(){} /** * XHR Polling constructor. * * @param {Object} opts * @api public */ function XHR(opts){ Polling.call(this, opts); if (global.location) { var isSSL = 'https:' == location.protocol; var port = location.port; // some user agents have empty `location.port` if (!port) { port = isSSL ? 443 : 80; } this.xd = opts.hostname != global.location.hostname || port != opts.port; this.xs = opts.secure != isSSL; } } /** * Inherits from Polling. */ inherit(XHR, Polling); /** * XHR supports binary */ XHR.prototype.supportsBinary = true; /** * Creates a request. * * @param {String} method * @api private */ XHR.prototype.request = function(opts){ opts = opts || {}; opts.uri = this.uri(); opts.xd = this.xd; opts.xs = this.xs; opts.agent = this.agent || false; opts.supportsBinary = this.supportsBinary; opts.enablesXDR = this.enablesXDR; return new Request(opts); }; /** * Sends data. * * @param {String} data to send. * @param {Function} called upon flush. * @api private */ XHR.prototype.doWrite = function(data, fn){ var isBinary = typeof data !== 'string' && data !== undefined; var req = this.request({ method: 'POST', data: data, isBinary: isBinary }); var self = this; req.on('success', fn); req.on('error', function(err){ self.onError('xhr post error', err); }); this.sendXhr = req; }; /** * Starts a poll cycle. * * @api private */ XHR.prototype.doPoll = function(){ debug('xhr poll'); var req = this.request(); var self = this; req.on('data', function(data){ self.onData(data); }); req.on('error', function(err){ self.onError('xhr poll error', err); }); this.pollXhr = req; }; /** * Request constructor * * @param {Object} options * @api public */ function Request(opts){ this.method = opts.method || 'GET'; this.uri = opts.uri; this.xd = !!opts.xd; this.xs = !!opts.xs; this.async = false !== opts.async; this.data = undefined != opts.data ? opts.data : null; this.agent = opts.agent; this.isBinary = opts.isBinary; this.supportsBinary = opts.supportsBinary; this.enablesXDR = opts.enablesXDR; this.create(); } /** * Mix in `Emitter`. */ Emitter(Request.prototype); /** * Creates the XHR object and sends the request. * * @api private */ Request.prototype.create = function(){ var xhr = this.xhr = new XMLHttpRequest({ agent: this.agent, xdomain: this.xd, xscheme: this.xs, enablesXDR: this.enablesXDR }); var self = this; try { debug('xhr open %s: %s', this.method, this.uri); xhr.open(this.method, this.uri, this.async); if (this.supportsBinary) { // This has to be done after open because Firefox is stupid // http://stackoverflow.com/questions/13216903/get-binary-data-with-xmlhttprequest-in-a-firefox-extension xhr.responseType = 'arraybuffer'; } if ('POST' == this.method) { try { if (this.isBinary) { xhr.setRequestHeader('Content-type', 'application/octet-stream'); } else { xhr.setRequestHeader('Content-type', 'text/plain;charset=UTF-8'); } } catch (e) {} } // ie6 check if ('withCredentials' in xhr) { xhr.withCredentials = true; } if (this.hasXDR()) { xhr.onload = function(){ self.onLoad(); }; xhr.onerror = function(){ self.onError(xhr.responseText); }; } else { xhr.onreadystatechange = function(){ if (4 != xhr.readyState) return; if (200 == xhr.status || 1223 == xhr.status) { self.onLoad(); } else { // make sure the `error` event handler that's user-set // does not throw in the same tick and gets caught here setTimeout(function(){ self.onError(xhr.status); }, 0); } }; } debug('xhr data %s', this.data); xhr.send(this.data); } catch (e) { // Need to defer since .create() is called directly fhrom the constructor // and thus the 'error' event can only be only bound *after* this exception // occurs. Therefore, also, we cannot throw here at all. setTimeout(function() { self.onError(e); }, 0); return; } if (global.document) { this.index = Request.requestsCount++; Request.requests[this.index] = this; } }; /** * Called upon successful response. * * @api private */ Request.prototype.onSuccess = function(){ this.emit('success'); this.cleanup(); }; /** * Called if we have data. * * @api private */ Request.prototype.onData = function(data){ this.emit('data', data); this.onSuccess(); }; /** * Called upon error. * * @api private */ Request.prototype.onError = function(err){ this.emit('error', err); this.cleanup(); }; /** * Cleans up house. * * @api private */ Request.prototype.cleanup = function(){ if ('undefined' == typeof this.xhr || null === this.xhr) { return; } // xmlhttprequest if (this.hasXDR()) { this.xhr.onload = this.xhr.onerror = empty; } else { this.xhr.onreadystatechange = empty; } try { this.xhr.abort(); } catch(e) {} if (global.document) { delete Request.requests[this.index]; } this.xhr = null; }; /** * Called upon load. * * @api private */ Request.prototype.onLoad = function(){ var data; try { var contentType; try { contentType = this.xhr.getResponseHeader('Content-Type').split(';')[0]; } catch (e) {} if (contentType === 'application/octet-stream') { data = this.xhr.response; } else { if (!this.supportsBinary) { data = this.xhr.responseText; } else { data = 'ok'; } } } catch (e) { this.onError(e); } if (null != data) { this.onData(data); } }; /** * Check if it has XDomainRequest. * * @api private */ Request.prototype.hasXDR = function(){ return 'undefined' !== typeof global.XDomainRequest && !this.xs && this.enablesXDR; }; /** * Aborts the request. * * @api public */ Request.prototype.abort = function(){ this.cleanup(); }; /** * Aborts pending requests when unloading the window. This is needed to prevent * memory leaks (e.g. when using IE) and to ensure that no spurious error is * emitted. */ if (global.document) { Request.requestsCount = 0; Request.requests = {}; if (global.attachEvent) { global.attachEvent('onunload', unloadHandler); } else if (global.addEventListener) { global.addEventListener('beforeunload', unloadHandler); } } function unloadHandler() { for (var i in Request.requests) { if (Request.requests.hasOwnProperty(i)) { Request.requests[i].abort(); } } }