Skip to content

Commit

Permalink
feat: add details to the "close" event
Browse files Browse the repository at this point in the history
The close event will now include additional details to help debugging
if anything has gone wrong.

Example when a payload is over the maxHttpBufferSize value in HTTP
long-polling mode:

```js
socket.on("close", (reason, details) => {
  console.log(reason); // "transport error"

  // in that case, details is an error object
  console.log(details.message); "xhr post error"
  console.log(details.description); // 413 (the HTTP status of the response)

  // details.context refers to the XMLHttpRequest object
  console.log(details.context.status); // 413
  console.log(details.context.responseText); // ""
});
```

Note: the error object was already included before this commit and is
kept for backward compatibility

Example when a payload is over the maxHttpBufferSize value with
WebSockets:

```js
socket.on("close", (reason, details) => {
  console.log(reason); // "transport close"

  // in that case, details is a plain object
  console.log(details.description); // "websocket connection closed"

  // details.context is a CloseEvent object
  console.log(details.context.code); // 1009 (which means "Message Too Big")
  console.log(details.context.reason); // ""
});
```

Example within a cluster without sticky sessions:

```js
socket.on("close", (reason, details) => {
  console.log(details.context.status); // 400
  console.log(details.context.responseText); // '{"code":1,"message":"Session ID unknown"}'
});
```

Note: we could also print some warnings in development for the "usual"
errors:

- CORS error
- HTTP 400 with multiple nodes
- HTTP 413 with maxHttpBufferSize

but that would require an additional step when going to production
(i.e. setting NODE_ENV variable to "production"). This is open to
discussion!

Related:

- socketio/socket.io#3946
- socketio/socket.io#1979
- socketio/socket.io-client#1518
  • Loading branch information
darrachequesne committed Apr 11, 2022
1 parent 6e1bbff commit b9252e2
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 46 deletions.
15 changes: 8 additions & 7 deletions lib/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import parseuri from "parseuri";
import debugModule from "debug"; // debug()
import { Emitter } from "@socket.io/component-emitter";
import { protocol } from "engine.io-parser";
import { CloseDetails } from "./transport";

const debug = debugModule("engine.io-client:socket"); // debug()

Expand Down Expand Up @@ -230,7 +231,7 @@ interface SocketReservedEvents {
upgrading: (transport) => void;
upgrade: (transport) => void;
upgradeError: (err: Error) => void;
close: (reason: string, desc?: Error) => void;
close: (reason: string, description?: CloseDetails | Error) => void;
}

export class Socket extends Emitter<{}, {}, SocketReservedEvents> {
Expand Down Expand Up @@ -365,7 +366,9 @@ export class Socket extends Emitter<{}, {}, SocketReservedEvents> {
}
if (this.hostname !== "localhost") {
this.offlineEventListener = () => {
this.onClose("transport close");
this.onClose("transport close", {
description: "network connection lost"
});
};
addEventListener("offline", this.offlineEventListener, false);
}
Expand Down Expand Up @@ -471,9 +474,7 @@ export class Socket extends Emitter<{}, {}, SocketReservedEvents> {
.on("drain", this.onDrain.bind(this))
.on("packet", this.onPacket.bind(this))
.on("error", this.onError.bind(this))
.on("close", () => {
this.onClose("transport close");
});
.on("close", reason => this.onClose("transport close", reason));
}

/**
Expand Down Expand Up @@ -890,7 +891,7 @@ export class Socket extends Emitter<{}, {}, SocketReservedEvents> {
*
* @api private
*/
private onClose(reason: string, desc?) {
private onClose(reason: string, description?: CloseDetails | Error) {
if (
"opening" === this.readyState ||
"open" === this.readyState ||
Expand Down Expand Up @@ -921,7 +922,7 @@ export class Socket extends Emitter<{}, {}, SocketReservedEvents> {
this.id = null;

// emit close event
this.emitReserved("close", reason, desc);
this.emitReserved("close", reason, description);

// clean buffers after, so users can still
// grab the buffers on `close` event
Expand Down
64 changes: 46 additions & 18 deletions lib/transport.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,42 @@
import { decodePacket } from "engine.io-parser";
import { DefaultEventsMap, Emitter } from "@socket.io/component-emitter";
import { decodePacket, Packet, RawData } from "engine.io-parser";
import { Emitter } from "@socket.io/component-emitter";
import { installTimerFunctions } from "./util.js";
import debugModule from "debug"; // debug()
import { SocketOptions } from "./socket.js";

const debug = debugModule("engine.io-client:transport"); // debug()

class TransportError extends Error {
public readonly type = "TransportError";

constructor(
reason: string,
readonly description: any,
readonly context: any
) {
super(reason);
}
}

export interface CloseDetails {
description: string;
context?: CloseEvent | XMLHttpRequest;
}

interface TransportReservedEvents {
open: () => void;
error: (err: TransportError) => void;
packet: (packet: Packet) => void;
close: (details?: CloseDetails) => void;
poll: () => void;
pollComplete: () => void;
drain: () => void;
}

export abstract class Transport extends Emitter<
DefaultEventsMap,
DefaultEventsMap
{},
{},
TransportReservedEvents
> {
protected opts: SocketOptions;
protected supportsBinary: boolean;
Expand Down Expand Up @@ -37,17 +65,17 @@ export abstract class Transport extends Emitter<
/**
* Emits an error.
*
* @param {String} str
* @param {String} reason
* @param description
* @param context - the error context
* @return {Transport} for chaining
* @api protected
*/
protected onError(msg, desc) {
const err = new Error(msg);
// @ts-ignore
err.type = "TransportError";
// @ts-ignore
err.description = desc;
super.emit("error", err);
protected onError(reason: string, description: any, context?: any) {
super.emitReserved(
"error",
new TransportError(reason, description, context)
);
return this;
}

Expand Down Expand Up @@ -102,7 +130,7 @@ export abstract class Transport extends Emitter<
protected onOpen() {
this.readyState = "open";
this.writable = true;
super.emit("open");
super.emitReserved("open");
}

/**
Expand All @@ -111,7 +139,7 @@ export abstract class Transport extends Emitter<
* @param {String} data
* @api protected
*/
protected onData(data) {
protected onData(data: RawData) {
const packet = decodePacket(data, this.socket.binaryType);
this.onPacket(packet);
}
Expand All @@ -121,18 +149,18 @@ export abstract class Transport extends Emitter<
*
* @api protected
*/
protected onPacket(packet) {
super.emit("packet", packet);
protected onPacket(packet: Packet) {
super.emitReserved("packet", packet);
}

/**
* Called upon close.
*
* @api protected
*/
protected onClose() {
protected onClose(details?: CloseDetails) {
this.readyState = "closed";
super.emit("close");
super.emitReserved("close", details);
}

protected abstract doOpen();
Expand Down
26 changes: 17 additions & 9 deletions lib/transports/polling-xhr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { installTimerFunctions, pick } from "../util.js";
import { DefaultEventsMap, Emitter } from "@socket.io/component-emitter";
import { Polling } from "./polling.js";
import { SocketOptions } from "../socket.js";
import { RawData } from "engine.io-parser";
import { CloseDetails } from "../transport";

const debug = debugModule("engine.io-client:polling-xhr"); // debug()

Expand Down Expand Up @@ -83,8 +85,8 @@ export class XHR extends Polling {
data: data
});
req.on("success", fn);
req.on("error", err => {
this.onError("xhr post error", err);
req.on("error", (xhrStatus, context) => {
this.onError("xhr post error", xhrStatus, context);
});
}

Expand All @@ -97,14 +99,20 @@ export class XHR extends Polling {
debug("xhr poll");
const req = this.request();
req.on("data", this.onData.bind(this));
req.on("error", err => {
this.onError("xhr poll error", err);
req.on("error", (xhrStatus, context) => {
this.onError("xhr poll error", xhrStatus, context);
});
this.pollXhr = req;
}
}

export class Request extends Emitter<DefaultEventsMap, DefaultEventsMap> {
interface RequestReservedEvents {
success: () => void;
data: (data: RawData) => void;
error: (err: number | Error, context: XMLHttpRequest) => void;
}

export class Request extends Emitter<{}, {}, RequestReservedEvents> {
private readonly opts: { xd; xs } & SocketOptions;
private readonly method: string;
private readonly uri: string;
Expand Down Expand Up @@ -230,7 +238,7 @@ export class Request extends Emitter<DefaultEventsMap, DefaultEventsMap> {
* @api private
*/
onSuccess() {
this.emit("success");
this.emitReserved("success");
this.cleanup();
}

Expand All @@ -240,7 +248,7 @@ export class Request extends Emitter<DefaultEventsMap, DefaultEventsMap> {
* @api private
*/
onData(data) {
this.emit("data", data);
this.emitReserved("data", data);
this.onSuccess();
}

Expand All @@ -249,8 +257,8 @@ export class Request extends Emitter<DefaultEventsMap, DefaultEventsMap> {
*
* @api private
*/
onError(err) {
this.emit("error", err);
onError(err: number | Error) {
this.emitReserved("error", err, this.xhr);
this.cleanup(true);
}

Expand Down
8 changes: 4 additions & 4 deletions lib/transports/polling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export abstract class Polling extends Transport {
debug("polling");
this.polling = true;
this.doPoll();
this.emit("poll");
this.emitReserved("poll");
}

/**
Expand All @@ -93,7 +93,7 @@ export abstract class Polling extends Transport {

// if its a close packet, we close the ongoing requests
if ("close" === packet.type) {
this.onClose();
this.onClose({ description: "transport closed by the server" });
return false;
}

Expand All @@ -108,7 +108,7 @@ export abstract class Polling extends Transport {
if ("closed" !== this.readyState) {
// if we got data we're not polling
this.polling = false;
this.emit("pollComplete");
this.emitReserved("pollComplete");

if ("open" === this.readyState) {
this.poll();
Expand Down Expand Up @@ -153,7 +153,7 @@ export abstract class Polling extends Transport {
encodePayload(packets, data => {
this.doWrite(data, () => {
this.writable = true;
this.emit("drain");
this.emitReserved("drain");
});
});
}
Expand Down
10 changes: 7 additions & 3 deletions lib/transports/websocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export class WS extends Transport {
: new WebSocket(uri)
: new WebSocket(uri, protocols, opts);
} catch (err) {
return this.emit("error", err);
return this.emitReserved("error", err);
}

this.ws.binaryType = this.socket.binaryType || defaultBinaryType;
Expand All @@ -111,7 +111,11 @@ export class WS extends Transport {
}
this.onOpen();
};
this.ws.onclose = this.onClose.bind(this);
this.ws.onclose = closeEvent =>
this.onClose({
description: "websocket connection closed",
context: closeEvent
});
this.ws.onmessage = ev => this.onData(ev.data);
this.ws.onerror = e => this.onError("websocket error", e);
}
Expand Down Expand Up @@ -168,7 +172,7 @@ export class WS extends Transport {
// defer to next tick to allow Socket to clear writeBuffer
nextTick(() => {
this.writable = true;
this.emit("drain");
this.emitReserved("drain");
}, this.setTimeoutFn);
}
});
Expand Down
Loading

0 comments on commit b9252e2

Please sign in to comment.