polling-xhr.js 7.6 KB

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