polling-xhr.js (9285B)
1 /* global attachEvent */ 2 3 /** 4 * Module requirements. 5 */ 6 7 var XMLHttpRequest = require('xmlhttprequest-ssl'); 8 var Polling = require('./polling'); 9 var Emitter = require('component-emitter'); 10 var inherit = require('component-inherit'); 11 var debug = require('debug')('engine.io-client:polling-xhr'); 12 var globalThis = require('../globalThis'); 13 14 /** 15 * Module exports. 16 */ 17 18 module.exports = XHR; 19 module.exports.Request = Request; 20 21 /** 22 * Empty function 23 */ 24 25 function empty () {} 26 27 /** 28 * XHR Polling constructor. 29 * 30 * @param {Object} opts 31 * @api public 32 */ 33 34 function XHR (opts) { 35 Polling.call(this, opts); 36 this.requestTimeout = opts.requestTimeout; 37 this.extraHeaders = opts.extraHeaders; 38 39 if (typeof location !== 'undefined') { 40 var isSSL = 'https:' === location.protocol; 41 var port = location.port; 42 43 // some user agents have empty `location.port` 44 if (!port) { 45 port = isSSL ? 443 : 80; 46 } 47 48 this.xd = (typeof location !== 'undefined' && opts.hostname !== location.hostname) || 49 port !== opts.port; 50 this.xs = opts.secure !== isSSL; 51 } 52 } 53 54 /** 55 * Inherits from Polling. 56 */ 57 58 inherit(XHR, Polling); 59 60 /** 61 * XHR supports binary 62 */ 63 64 XHR.prototype.supportsBinary = true; 65 66 /** 67 * Creates a request. 68 * 69 * @param {String} method 70 * @api private 71 */ 72 73 XHR.prototype.request = function (opts) { 74 opts = opts || {}; 75 opts.uri = this.uri(); 76 opts.xd = this.xd; 77 opts.xs = this.xs; 78 opts.agent = this.agent || false; 79 opts.supportsBinary = this.supportsBinary; 80 opts.enablesXDR = this.enablesXDR; 81 opts.withCredentials = this.withCredentials; 82 83 // SSL options for Node.js client 84 opts.pfx = this.pfx; 85 opts.key = this.key; 86 opts.passphrase = this.passphrase; 87 opts.cert = this.cert; 88 opts.ca = this.ca; 89 opts.ciphers = this.ciphers; 90 opts.rejectUnauthorized = this.rejectUnauthorized; 91 opts.requestTimeout = this.requestTimeout; 92 93 // other options for Node.js client 94 opts.extraHeaders = this.extraHeaders; 95 96 return new Request(opts); 97 }; 98 99 /** 100 * Sends data. 101 * 102 * @param {String} data to send. 103 * @param {Function} called upon flush. 104 * @api private 105 */ 106 107 XHR.prototype.doWrite = function (data, fn) { 108 var isBinary = typeof data !== 'string' && data !== undefined; 109 var req = this.request({ method: 'POST', data: data, isBinary: isBinary }); 110 var self = this; 111 req.on('success', fn); 112 req.on('error', function (err) { 113 self.onError('xhr post error', err); 114 }); 115 this.sendXhr = req; 116 }; 117 118 /** 119 * Starts a poll cycle. 120 * 121 * @api private 122 */ 123 124 XHR.prototype.doPoll = function () { 125 debug('xhr poll'); 126 var req = this.request(); 127 var self = this; 128 req.on('data', function (data) { 129 self.onData(data); 130 }); 131 req.on('error', function (err) { 132 self.onError('xhr poll error', err); 133 }); 134 this.pollXhr = req; 135 }; 136 137 /** 138 * Request constructor 139 * 140 * @param {Object} options 141 * @api public 142 */ 143 144 function Request (opts) { 145 this.method = opts.method || 'GET'; 146 this.uri = opts.uri; 147 this.xd = !!opts.xd; 148 this.xs = !!opts.xs; 149 this.async = false !== opts.async; 150 this.data = undefined !== opts.data ? opts.data : null; 151 this.agent = opts.agent; 152 this.isBinary = opts.isBinary; 153 this.supportsBinary = opts.supportsBinary; 154 this.enablesXDR = opts.enablesXDR; 155 this.withCredentials = opts.withCredentials; 156 this.requestTimeout = opts.requestTimeout; 157 158 // SSL options for Node.js client 159 this.pfx = opts.pfx; 160 this.key = opts.key; 161 this.passphrase = opts.passphrase; 162 this.cert = opts.cert; 163 this.ca = opts.ca; 164 this.ciphers = opts.ciphers; 165 this.rejectUnauthorized = opts.rejectUnauthorized; 166 167 // other options for Node.js client 168 this.extraHeaders = opts.extraHeaders; 169 170 this.create(); 171 } 172 173 /** 174 * Mix in `Emitter`. 175 */ 176 177 Emitter(Request.prototype); 178 179 /** 180 * Creates the XHR object and sends the request. 181 * 182 * @api private 183 */ 184 185 Request.prototype.create = function () { 186 var opts = { agent: this.agent, xdomain: this.xd, xscheme: this.xs, enablesXDR: this.enablesXDR }; 187 188 // SSL options for Node.js client 189 opts.pfx = this.pfx; 190 opts.key = this.key; 191 opts.passphrase = this.passphrase; 192 opts.cert = this.cert; 193 opts.ca = this.ca; 194 opts.ciphers = this.ciphers; 195 opts.rejectUnauthorized = this.rejectUnauthorized; 196 197 var xhr = this.xhr = new XMLHttpRequest(opts); 198 var self = this; 199 200 try { 201 debug('xhr open %s: %s', this.method, this.uri); 202 xhr.open(this.method, this.uri, this.async); 203 try { 204 if (this.extraHeaders) { 205 xhr.setDisableHeaderCheck && xhr.setDisableHeaderCheck(true); 206 for (var i in this.extraHeaders) { 207 if (this.extraHeaders.hasOwnProperty(i)) { 208 xhr.setRequestHeader(i, this.extraHeaders[i]); 209 } 210 } 211 } 212 } catch (e) {} 213 214 if ('POST' === this.method) { 215 try { 216 if (this.isBinary) { 217 xhr.setRequestHeader('Content-type', 'application/octet-stream'); 218 } else { 219 xhr.setRequestHeader('Content-type', 'text/plain;charset=UTF-8'); 220 } 221 } catch (e) {} 222 } 223 224 try { 225 xhr.setRequestHeader('Accept', '*/*'); 226 } catch (e) {} 227 228 // ie6 check 229 if ('withCredentials' in xhr) { 230 xhr.withCredentials = this.withCredentials; 231 } 232 233 if (this.requestTimeout) { 234 xhr.timeout = this.requestTimeout; 235 } 236 237 if (this.hasXDR()) { 238 xhr.onload = function () { 239 self.onLoad(); 240 }; 241 xhr.onerror = function () { 242 self.onError(xhr.responseText); 243 }; 244 } else { 245 xhr.onreadystatechange = function () { 246 if (xhr.readyState === 2) { 247 try { 248 var contentType = xhr.getResponseHeader('Content-Type'); 249 if (self.supportsBinary && contentType === 'application/octet-stream' || contentType === 'application/octet-stream; charset=UTF-8') { 250 xhr.responseType = 'arraybuffer'; 251 } 252 } catch (e) {} 253 } 254 if (4 !== xhr.readyState) return; 255 if (200 === xhr.status || 1223 === xhr.status) { 256 self.onLoad(); 257 } else { 258 // make sure the `error` event handler that's user-set 259 // does not throw in the same tick and gets caught here 260 setTimeout(function () { 261 self.onError(typeof xhr.status === 'number' ? xhr.status : 0); 262 }, 0); 263 } 264 }; 265 } 266 267 debug('xhr data %s', this.data); 268 xhr.send(this.data); 269 } catch (e) { 270 // Need to defer since .create() is called directly fhrom the constructor 271 // and thus the 'error' event can only be only bound *after* this exception 272 // occurs. Therefore, also, we cannot throw here at all. 273 setTimeout(function () { 274 self.onError(e); 275 }, 0); 276 return; 277 } 278 279 if (typeof document !== 'undefined') { 280 this.index = Request.requestsCount++; 281 Request.requests[this.index] = this; 282 } 283 }; 284 285 /** 286 * Called upon successful response. 287 * 288 * @api private 289 */ 290 291 Request.prototype.onSuccess = function () { 292 this.emit('success'); 293 this.cleanup(); 294 }; 295 296 /** 297 * Called if we have data. 298 * 299 * @api private 300 */ 301 302 Request.prototype.onData = function (data) { 303 this.emit('data', data); 304 this.onSuccess(); 305 }; 306 307 /** 308 * Called upon error. 309 * 310 * @api private 311 */ 312 313 Request.prototype.onError = function (err) { 314 this.emit('error', err); 315 this.cleanup(true); 316 }; 317 318 /** 319 * Cleans up house. 320 * 321 * @api private 322 */ 323 324 Request.prototype.cleanup = function (fromError) { 325 if ('undefined' === typeof this.xhr || null === this.xhr) { 326 return; 327 } 328 // xmlhttprequest 329 if (this.hasXDR()) { 330 this.xhr.onload = this.xhr.onerror = empty; 331 } else { 332 this.xhr.onreadystatechange = empty; 333 } 334 335 if (fromError) { 336 try { 337 this.xhr.abort(); 338 } catch (e) {} 339 } 340 341 if (typeof document !== 'undefined') { 342 delete Request.requests[this.index]; 343 } 344 345 this.xhr = null; 346 }; 347 348 /** 349 * Called upon load. 350 * 351 * @api private 352 */ 353 354 Request.prototype.onLoad = function () { 355 var data; 356 try { 357 var contentType; 358 try { 359 contentType = this.xhr.getResponseHeader('Content-Type'); 360 } catch (e) {} 361 if (contentType === 'application/octet-stream' || contentType === 'application/octet-stream; charset=UTF-8') { 362 data = this.xhr.response || this.xhr.responseText; 363 } else { 364 data = this.xhr.responseText; 365 } 366 } catch (e) { 367 this.onError(e); 368 } 369 if (null != data) { 370 this.onData(data); 371 } 372 }; 373 374 /** 375 * Check if it has XDomainRequest. 376 * 377 * @api private 378 */ 379 380 Request.prototype.hasXDR = function () { 381 return typeof XDomainRequest !== 'undefined' && !this.xs && this.enablesXDR; 382 }; 383 384 /** 385 * Aborts the request. 386 * 387 * @api public 388 */ 389 390 Request.prototype.abort = function () { 391 this.cleanup(); 392 }; 393 394 /** 395 * Aborts pending requests when unloading the window. This is needed to prevent 396 * memory leaks (e.g. when using IE) and to ensure that no spurious error is 397 * emitted. 398 */ 399 400 Request.requestsCount = 0; 401 Request.requests = {}; 402 403 if (typeof document !== 'undefined') { 404 if (typeof attachEvent === 'function') { 405 attachEvent('onunload', unloadHandler); 406 } else if (typeof addEventListener === 'function') { 407 var terminationEvent = 'onpagehide' in globalThis ? 'pagehide' : 'unload'; 408 addEventListener(terminationEvent, unloadHandler, false); 409 } 410 } 411 412 function unloadHandler () { 413 for (var i in Request.requests) { 414 if (Request.requests.hasOwnProperty(i)) { 415 Request.requests[i].abort(); 416 } 417 } 418 }