polling-xhr.js 9.1 KB

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