You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

351 lines
6.9 KiB

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