feat: add HTTP long-polling implementation based on fetch()

Usage:

```js
import { Socket, transports, Fetch } from "engine.io-client";

transports.polling = Fetch;

const socket = new Socket("https://example.com");
```

Note: tree-shaking unused transports is not currently supported and
will be added later.

Related:

- https://github.com/socketio/socket.io/issues/4980
- https://github.com/socketio/engine.io-client/issues/716
This commit is contained in:
Damien Arrachequesne
2024-04-23 11:03:42 +02:00
parent 218c3443f6
commit b11763beec
13 changed files with 499 additions and 333 deletions

View File

@@ -30,3 +30,7 @@ jobs:
- name: Run tests
run: npm test
- name: Run tests with fetch()
run: npm run test:node-fetch
if: ${{ matrix.node-version == '20' }} # fetch() was added in Node.js v18.0.0 (without experimental flag)

View File

@@ -8,3 +8,6 @@ export { transports } from "./transports/index.js";
export { installTimerFunctions } from "./util.js";
export { parse } from "./contrib/parseuri.js";
export { nextTick } from "./transports/websocket-constructor.js";
export { Fetch } from "./transports/polling-fetch.js";
export { XHR } from "./transports/polling-xhr.js";

View File

@@ -1,9 +1,9 @@
import { Polling } from "./polling.js";
import { XHR } from "./polling-xhr.js";
import { WS } from "./websocket.js";
import { WT } from "./webtransport.js";
export const transports = {
websocket: WS,
webtransport: WT,
polling: Polling,
polling: XHR,
};

View File

@@ -0,0 +1,72 @@
import { Polling } from "./polling.js";
import { CookieJar, createCookieJar } from "./xmlhttprequest.js";
/**
* HTTP long-polling based on `fetch()`
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/fetch
*/
export class Fetch extends Polling {
private readonly cookieJar?: CookieJar;
constructor(opts) {
super(opts);
if (this.opts.withCredentials) {
this.cookieJar = createCookieJar();
}
}
override doPoll() {
this._fetch()
.then((res) => {
if (!res.ok) {
return this.onError("fetch read error", res.status, res);
}
res.text().then((data) => this.onData(data));
})
.catch((err) => {
this.onError("fetch read error", err);
});
}
override doWrite(data: string, callback: () => void) {
this._fetch(data)
.then((res) => {
if (!res.ok) {
return this.onError("fetch write error", res.status, res);
}
callback();
})
.catch((err) => {
this.onError("fetch write error", err);
});
}
private _fetch(data?: string) {
const isPost = data !== undefined;
const headers = new Headers(this.opts.extraHeaders);
if (isPost) {
headers.set("content-type", "text/plain;charset=UTF-8");
}
this.cookieJar?.appendCookies(headers);
return fetch(this.uri(), {
method: isPost ? "POST" : "GET",
body: isPost ? data : null,
headers,
credentials: this.opts.withCredentials ? "include" : "omit",
}).then((res) => {
if (this.cookieJar) {
// @ts-ignore getSetCookie() was added in Node.js v19.7.0
this.cookieJar.parseCookies(res.headers.getSetCookie());
}
return res;
});
}
}

View File

@@ -0,0 +1,327 @@
import { Polling } from "./polling.js";
import {
CookieJar,
createCookieJar,
XHR as XMLHttpRequest,
} from "./xmlhttprequest.js";
import { Emitter } from "@socket.io/component-emitter";
import type { SocketOptions } from "../socket.js";
import { installTimerFunctions, pick } from "../util.js";
import { globalThisShim as globalThis } from "../globalThis.js";
import type { RawData } from "engine.io-parser";
import debugModule from "debug"; // debug()
const debug = debugModule("engine.io-client:polling"); // debug()
function empty() {}
const hasXHR2 = (function () {
const xhr = new XMLHttpRequest({
xdomain: false,
});
return null != xhr.responseType;
})();
/**
* HTTP long-polling based on `XMLHttpRequest`
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
*/
export class XHR extends Polling {
private readonly xd: boolean;
private pollXhr: any;
private cookieJar?: CookieJar;
/**
* XHR Polling constructor.
*
* @param {Object} opts
* @package
*/
constructor(opts) {
super(opts);
if (typeof location !== "undefined") {
const isSSL = "https:" === location.protocol;
let port = location.port;
// some user agents have empty `location.port`
if (!port) {
port = isSSL ? "443" : "80";
}
this.xd =
(typeof location !== "undefined" &&
opts.hostname !== location.hostname) ||
port !== opts.port;
}
/**
* XHR supports binary
*/
const forceBase64 = opts && opts.forceBase64;
this.supportsBinary = hasXHR2 && !forceBase64;
if (this.opts.withCredentials) {
this.cookieJar = createCookieJar();
}
}
/**
* Creates a request.
*
* @param {String} method
* @private
*/
request(opts = {}) {
Object.assign(opts, { xd: this.xd, cookieJar: this.cookieJar }, this.opts);
return new Request(this.uri(), opts);
}
/**
* Sends data.
*
* @param {String} data to send.
* @param {Function} called upon flush.
* @private
*/
override doWrite(data, fn) {
const req = this.request({
method: "POST",
data: data,
});
req.on("success", fn);
req.on("error", (xhrStatus, context) => {
this.onError("xhr post error", xhrStatus, context);
});
}
/**
* Starts a poll cycle.
*
* @private
*/
override doPoll() {
debug("xhr poll");
const req = this.request();
req.on("data", this.onData.bind(this));
req.on("error", (xhrStatus, context) => {
this.onError("xhr poll error", xhrStatus, context);
});
this.pollXhr = req;
}
}
interface RequestReservedEvents {
success: () => void;
data: (data: RawData) => void;
error: (err: number | Error, context: unknown) => void; // context should be typed as XMLHttpRequest, but this type is not available on non-browser platforms
}
export class Request extends Emitter<{}, {}, RequestReservedEvents> {
private readonly opts: { xd; cookieJar: CookieJar } & SocketOptions;
private readonly method: string;
private readonly uri: string;
private readonly data: string | ArrayBuffer;
private xhr: any;
private setTimeoutFn: typeof setTimeout;
private index: number;
static requestsCount = 0;
static requests = {};
/**
* Request constructor
*
* @param {Object} options
* @package
*/
constructor(uri, opts) {
super();
installTimerFunctions(this, opts);
this.opts = opts;
this.method = opts.method || "GET";
this.uri = uri;
this.data = undefined !== opts.data ? opts.data : null;
this.create();
}
/**
* Creates the XHR object and sends the request.
*
* @private
*/
private create() {
const opts = pick(
this.opts,
"agent",
"pfx",
"key",
"passphrase",
"cert",
"ca",
"ciphers",
"rejectUnauthorized",
"autoUnref"
);
opts.xdomain = !!this.opts.xd;
const xhr = (this.xhr = new XMLHttpRequest(opts));
try {
debug("xhr open %s: %s", this.method, this.uri);
xhr.open(this.method, this.uri, true);
try {
if (this.opts.extraHeaders) {
xhr.setDisableHeaderCheck && xhr.setDisableHeaderCheck(true);
for (let i in this.opts.extraHeaders) {
if (this.opts.extraHeaders.hasOwnProperty(i)) {
xhr.setRequestHeader(i, this.opts.extraHeaders[i]);
}
}
}
} catch (e) {}
if ("POST" === this.method) {
try {
xhr.setRequestHeader("Content-type", "text/plain;charset=UTF-8");
} catch (e) {}
}
try {
xhr.setRequestHeader("Accept", "*/*");
} catch (e) {}
this.opts.cookieJar?.addCookies(xhr);
// ie6 check
if ("withCredentials" in xhr) {
xhr.withCredentials = this.opts.withCredentials;
}
if (this.opts.requestTimeout) {
xhr.timeout = this.opts.requestTimeout;
}
xhr.onreadystatechange = () => {
if (xhr.readyState === 3) {
this.opts.cookieJar?.parseCookies(
xhr.getResponseHeader("set-cookie")
);
}
if (4 !== xhr.readyState) return;
if (200 === xhr.status || 1223 === xhr.status) {
this.onLoad();
} else {
// make sure the `error` event handler that's user-set
// does not throw in the same tick and gets caught here
this.setTimeoutFn(() => {
this.onError(typeof xhr.status === "number" ? xhr.status : 0);
}, 0);
}
};
debug("xhr data %s", this.data);
xhr.send(this.data);
} catch (e) {
// Need to defer since .create() is called directly from the constructor
// and thus the 'error' event can only be only bound *after* this exception
// occurs. Therefore, also, we cannot throw here at all.
this.setTimeoutFn(() => {
this.onError(e);
}, 0);
return;
}
if (typeof document !== "undefined") {
this.index = Request.requestsCount++;
Request.requests[this.index] = this;
}
}
/**
* Called upon error.
*
* @private
*/
private onError(err: number | Error) {
this.emitReserved("error", err, this.xhr);
this.cleanup(true);
}
/**
* Cleans up house.
*
* @private
*/
private cleanup(fromError?) {
if ("undefined" === typeof this.xhr || null === this.xhr) {
return;
}
this.xhr.onreadystatechange = empty;
if (fromError) {
try {
this.xhr.abort();
} catch (e) {}
}
if (typeof document !== "undefined") {
delete Request.requests[this.index];
}
this.xhr = null;
}
/**
* Called upon load.
*
* @private
*/
private onLoad() {
const data = this.xhr.responseText;
if (data !== null) {
this.emitReserved("data", data);
this.emitReserved("success");
this.cleanup();
}
}
/**
* Aborts the request.
*
* @package
*/
public abort() {
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 (typeof document !== "undefined") {
// @ts-ignore
if (typeof attachEvent === "function") {
// @ts-ignore
attachEvent("onunload", unloadHandler);
} else if (typeof addEventListener === "function") {
const terminationEvent = "onpagehide" in globalThis ? "pagehide" : "unload";
addEventListener(terminationEvent, unloadHandler, false);
}
}
function unloadHandler() {
for (let i in Request.requests) {
if (Request.requests.hasOwnProperty(i)) {
Request.requests[i].abort();
}
}
}

View File

@@ -1,69 +1,12 @@
import { Transport } from "../transport.js";
import debugModule from "debug"; // debug()
import { yeast } from "../contrib/yeast.js";
import { encode } from "../contrib/parseqs.js";
import { encodePayload, decodePayload, RawData } from "engine.io-parser";
import {
CookieJar,
createCookieJar,
XHR as XMLHttpRequest,
} from "./xmlhttprequest.js";
import { Emitter } from "@socket.io/component-emitter";
import { SocketOptions } from "../socket.js";
import { installTimerFunctions, pick } from "../util.js";
import { globalThisShim as globalThis } from "../globalThis.js";
import { encodePayload, decodePayload } from "engine.io-parser";
import debugModule from "debug"; // debug()
const debug = debugModule("engine.io-client:polling"); // debug()
function empty() {}
const hasXHR2 = (function () {
const xhr = new XMLHttpRequest({
xdomain: false,
});
return null != xhr.responseType;
})();
export class Polling extends Transport {
private readonly xd: boolean;
export abstract class Polling extends Transport {
private polling: boolean = false;
private pollXhr: any;
private cookieJar?: CookieJar;
/**
* XHR Polling constructor.
*
* @param {Object} opts
* @package
*/
constructor(opts) {
super(opts);
if (typeof location !== "undefined") {
const isSSL = "https:" === location.protocol;
let port = location.port;
// some user agents have empty `location.port`
if (!port) {
port = isSSL ? "443" : "80";
}
this.xd =
(typeof location !== "undefined" &&
opts.hostname !== location.hostname) ||
port !== opts.port;
}
/**
* XHR supports binary
*/
const forceBase64 = opts && opts.forceBase64;
this.supportsBinary = hasXHR2 && !forceBase64;
if (this.opts.withCredentials) {
this.cookieJar = createCookieJar();
}
}
override get name() {
return "polling";
@@ -215,7 +158,7 @@ export class Polling extends Transport {
*
* @private
*/
private uri() {
protected uri() {
const schema = this.opts.secure ? "https" : "http";
const query: { b64?: number; sid?: string } = this.query || {};
@@ -231,259 +174,6 @@ export class Polling extends Transport {
return this.createUri(schema, query);
}
/**
* Creates a request.
*
* @param {String} method
* @private
*/
request(opts = {}) {
Object.assign(opts, { xd: this.xd, cookieJar: this.cookieJar }, this.opts);
return new Request(this.uri(), opts);
}
/**
* Sends data.
*
* @param {String} data to send.
* @param {Function} called upon flush.
* @private
*/
private doWrite(data, fn) {
const req = this.request({
method: "POST",
data: data,
});
req.on("success", fn);
req.on("error", (xhrStatus, context) => {
this.onError("xhr post error", xhrStatus, context);
});
}
/**
* Starts a poll cycle.
*
* @private
*/
private doPoll() {
debug("xhr poll");
const req = this.request();
req.on("data", this.onData.bind(this));
req.on("error", (xhrStatus, context) => {
this.onError("xhr poll error", xhrStatus, context);
});
this.pollXhr = req;
}
}
interface RequestReservedEvents {
success: () => void;
data: (data: RawData) => void;
error: (err: number | Error, context: unknown) => void; // context should be typed as XMLHttpRequest, but this type is not available on non-browser platforms
}
export class Request extends Emitter<{}, {}, RequestReservedEvents> {
private readonly opts: { xd; cookieJar: CookieJar } & SocketOptions;
private readonly method: string;
private readonly uri: string;
private readonly data: string | ArrayBuffer;
private xhr: any;
private setTimeoutFn: typeof setTimeout;
private index: number;
static requestsCount = 0;
static requests = {};
/**
* Request constructor
*
* @param {Object} options
* @package
*/
constructor(uri, opts) {
super();
installTimerFunctions(this, opts);
this.opts = opts;
this.method = opts.method || "GET";
this.uri = uri;
this.data = undefined !== opts.data ? opts.data : null;
this.create();
}
/**
* Creates the XHR object and sends the request.
*
* @private
*/
private create() {
const opts = pick(
this.opts,
"agent",
"pfx",
"key",
"passphrase",
"cert",
"ca",
"ciphers",
"rejectUnauthorized",
"autoUnref"
);
opts.xdomain = !!this.opts.xd;
const xhr = (this.xhr = new XMLHttpRequest(opts));
try {
debug("xhr open %s: %s", this.method, this.uri);
xhr.open(this.method, this.uri, true);
try {
if (this.opts.extraHeaders) {
xhr.setDisableHeaderCheck && xhr.setDisableHeaderCheck(true);
for (let i in this.opts.extraHeaders) {
if (this.opts.extraHeaders.hasOwnProperty(i)) {
xhr.setRequestHeader(i, this.opts.extraHeaders[i]);
}
}
}
} catch (e) {}
if ("POST" === this.method) {
try {
xhr.setRequestHeader("Content-type", "text/plain;charset=UTF-8");
} catch (e) {}
}
try {
xhr.setRequestHeader("Accept", "*/*");
} catch (e) {}
this.opts.cookieJar?.addCookies(xhr);
// ie6 check
if ("withCredentials" in xhr) {
xhr.withCredentials = this.opts.withCredentials;
}
if (this.opts.requestTimeout) {
xhr.timeout = this.opts.requestTimeout;
}
xhr.onreadystatechange = () => {
if (xhr.readyState === 3) {
this.opts.cookieJar?.parseCookies(xhr);
}
if (4 !== xhr.readyState) return;
if (200 === xhr.status || 1223 === xhr.status) {
this.onLoad();
} else {
// make sure the `error` event handler that's user-set
// does not throw in the same tick and gets caught here
this.setTimeoutFn(() => {
this.onError(typeof xhr.status === "number" ? xhr.status : 0);
}, 0);
}
};
debug("xhr data %s", this.data);
xhr.send(this.data);
} catch (e) {
// Need to defer since .create() is called directly from the constructor
// and thus the 'error' event can only be only bound *after* this exception
// occurs. Therefore, also, we cannot throw here at all.
this.setTimeoutFn(() => {
this.onError(e);
}, 0);
return;
}
if (typeof document !== "undefined") {
this.index = Request.requestsCount++;
Request.requests[this.index] = this;
}
}
/**
* Called upon error.
*
* @private
*/
private onError(err: number | Error) {
this.emitReserved("error", err, this.xhr);
this.cleanup(true);
}
/**
* Cleans up house.
*
* @private
*/
private cleanup(fromError?) {
if ("undefined" === typeof this.xhr || null === this.xhr) {
return;
}
this.xhr.onreadystatechange = empty;
if (fromError) {
try {
this.xhr.abort();
} catch (e) {}
}
if (typeof document !== "undefined") {
delete Request.requests[this.index];
}
this.xhr = null;
}
/**
* Called upon load.
*
* @private
*/
private onLoad() {
const data = this.xhr.responseText;
if (data !== null) {
this.emitReserved("data", data);
this.emitReserved("success");
this.cleanup();
}
}
/**
* Aborts the request.
*
* @package
*/
public abort() {
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 (typeof document !== "undefined") {
// @ts-ignore
if (typeof attachEvent === "function") {
// @ts-ignore
attachEvent("onunload", unloadHandler);
} else if (typeof addEventListener === "function") {
const terminationEvent = "onpagehide" in globalThis ? "pagehide" : "unload";
addEventListener(terminationEvent, unloadHandler, false);
}
}
function unloadHandler() {
for (let i in Request.requests) {
if (Request.requests.hasOwnProperty(i)) {
Request.requests[i].abort();
}
}
abstract doPoll();
abstract doWrite(data: string, callback: () => void);
}

View File

@@ -70,8 +70,7 @@ export function parse(setCookieString: string): Cookie {
export class CookieJar {
private cookies = new Map<string, Cookie>();
public parseCookies(xhr: any) {
const values = xhr.getResponseHeader("set-cookie");
public parseCookies(values: string[]) {
if (!values) {
return;
}
@@ -99,4 +98,14 @@ export class CookieJar {
xhr.setRequestHeader("cookie", cookies.join("; "));
}
}
public appendCookies(headers: Headers) {
this.cookies.forEach((cookie, name) => {
if (cookie.expires?.getTime() < Date.now()) {
this.cookies.delete(name);
} else {
headers.append("cookie", `${name}=${cookie.value}`);
}
});
}
}

31
package-lock.json generated
View File

@@ -24,6 +24,7 @@
"@rollup/plugin-commonjs": "^21.0.0",
"@rollup/plugin-node-resolve": "^13.0.5",
"@sinonjs/fake-timers": "^7.1.2",
"@types/debug": "^4.1.12",
"@types/mocha": "^9.0.0",
"@types/node": "^16.10.1",
"@types/sinonjs__fake-timers": "^6.0.3",
@@ -1535,6 +1536,15 @@
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==",
"dev": true
},
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
"dev": true,
"dependencies": {
"@types/ms": "*"
}
},
"node_modules/@types/estree": {
"version": "0.0.39",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
@@ -1553,6 +1563,12 @@
"integrity": "sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA==",
"dev": true
},
"node_modules/@types/ms": {
"version": "0.7.34",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==",
"dev": true
},
"node_modules/@types/node": {
"version": "16.18.35",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.35.tgz",
@@ -15508,6 +15524,15 @@
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==",
"dev": true
},
"@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
"dev": true,
"requires": {
"@types/ms": "*"
}
},
"@types/estree": {
"version": "0.0.39",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
@@ -15526,6 +15551,12 @@
"integrity": "sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA==",
"dev": true
},
"@types/ms": {
"version": "0.7.34",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==",
"dev": true
},
"@types/node": {
"version": "16.18.35",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.35.tgz",

View File

@@ -68,6 +68,7 @@
"@rollup/plugin-commonjs": "^21.0.0",
"@rollup/plugin-node-resolve": "^13.0.5",
"@sinonjs/fake-timers": "^7.1.2",
"@types/debug": "^4.1.12",
"@types/mocha": "^9.0.0",
"@types/node": "^16.10.1",
"@types/sinonjs__fake-timers": "^6.0.3",
@@ -94,6 +95,7 @@
"compile": "rimraf ./build && tsc && tsc -p tsconfig.esm.json && ./postcompile.sh",
"test": "npm run format:check && npm run compile && if test \"$BROWSERS\" = \"1\" ; then npm run test:browser; else npm run test:node; fi",
"test:node": "mocha --bail --require test/support/hooks.js test/index.js test/webtransport.mjs",
"test:node-fetch": "USE_FETCH=1 npm run test:node",
"test:browser": "zuul test/index.js",
"build": "rollup -c support/rollup.config.umd.js && rollup -c support/rollup.config.esm.js",
"format:check": "prettier --check 'lib/**/*.ts' 'test/**/*.js' 'test/webtransport.mjs' 'support/**/*.js'",

View File

@@ -199,7 +199,9 @@ describe("connection", function () {
if (env.browser && typeof addEventListener === "function") {
it("should close the socket when receiving a beforeunload event", (done) => {
const socket = new Socket();
const socket = new Socket({
closeOnBeforeunload: true,
});
const createEvent = (name) => {
if (typeof Event === "function") {

View File

@@ -1,6 +1,12 @@
const expect = require("expect.js");
const { Socket } = require("../");
const { isIE11, isAndroid, isEdge, isIPad } = require("./support/env");
const {
isIE11,
isAndroid,
isEdge,
isIPad,
useFetch,
} = require("./support/env");
const FakeTimers = require("@sinonjs/fake-timers");
const { repeat } = require("./util");
@@ -92,11 +98,15 @@ describe("Socket", function () {
socket.on("error", (err) => {
expect(err).to.be.an(Error);
expect(err.type).to.eql("TransportError");
expect(err.message).to.eql("xhr post error");
expect(err.description).to.eql(413);
// err.context is a XMLHttpRequest object
expect(err.context.readyState).to.eql(4);
expect(err.context.responseText).to.eql("");
if (useFetch) {
expect(err.message).to.eql("fetch write error");
} else {
expect(err.message).to.eql("xhr post error");
// err.context is a XMLHttpRequest object
expect(err.context.readyState).to.eql(4);
expect(err.context.responseText).to.eql("");
}
});
socket.on("close", (reason, details) => {
@@ -137,13 +147,17 @@ describe("Socket", function () {
socket.on("error", (err) => {
expect(err).to.be.an(Error);
expect(err.type).to.eql("TransportError");
expect(err.message).to.eql("xhr poll error");
expect(err.description).to.eql(400);
// err.context is a XMLHttpRequest object
expect(err.context.readyState).to.eql(4);
expect(err.context.responseText).to.eql(
'{"code":1,"message":"Session ID unknown"}'
);
if (useFetch) {
expect(err.message).to.eql("fetch read error");
} else {
expect(err.message).to.eql("xhr poll error");
// err.context is a XMLHttpRequest object
expect(err.context.readyState).to.eql(4);
expect(err.context.responseText).to.eql(
'{"code":1,"message":"Session ID unknown"}'
);
}
});
socket.on("close", (reason, details) => {

View File

@@ -27,3 +27,11 @@ if (typeof location === "undefined") {
port: 3000,
};
}
exports.useFetch = !exports.browser && process.env.USE_FETCH !== undefined;
if (exports.useFetch) {
console.warn("testing with fetch() instead of XMLHttpRequest");
const { transports, Fetch } = require("../..");
transports.polling = Fetch;
}

View File

@@ -222,7 +222,11 @@ describe("Transport", () => {
});
polling.doOpen();
});
it("should accept an `agent` option for XMLHttpRequest", (done) => {
it("should accept an `agent` option for XMLHttpRequest", function (done) {
if (env.useFetch) {
return this.skip();
}
const polling = new eio.transports.polling({
path: "/engine.io",
hostname: "localhost",