fix(goLive): gateway crash on screenshare stop — unhandledRejection during teardown
Test 00:32 confirmed the video pipeline WORKS (1410 frames @ 1280x720 sent, ready=true, camera off) but the gateway crashed at stream stop: unhandledRejection → graceful shutdown → systemd restart (bot offline). Root cause candidates (both were fire-and-forget promises without .catch): - BaseMediaConnection.setProtocols().then(...) — rejects when the PC is closed while setProtocols is in flight (stream teardown) - void webRtcConn.createOffer().then(...) — rejects when the PC closes while the offer is still gathering Fixes: - .catch on both promise chains (log + continue; teardown is expected) - unhandledRejection handler now treats transient stream errors (EPIPE, ERR_STREAM_DESTROYED, ERR_STREAM_WRITE_AFTER_END, ECONNRESET) like uncaughtException already does — warn + continue instead of shutting down the whole gateway. Non-transient rejections still log + shutdown (with String(reason) so the detail actually shows).
This commit is contained in:
@@ -303,7 +303,27 @@ export async function initializeDiscordGateway() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
process.on("unhandledRejection", (reason, promise) => {
|
process.on("unhandledRejection", (reason, promise) => {
|
||||||
logger.error({ reason, promise }, "Unhandled rejection");
|
const err =
|
||||||
|
reason instanceof Error ? reason : new Error(String(reason ?? "unknown"));
|
||||||
|
const code = (err as NodeJS.ErrnoException).code ?? "";
|
||||||
|
// Same transient-teardown policy as uncaughtException: a rejection that
|
||||||
|
// fires while a stream is being torn down (EPIPE after ffmpeg stdin
|
||||||
|
// closes, write-after-destroy, socket reset) must NOT take the whole
|
||||||
|
// gateway offline. Log detail + continue. Everything else still shuts
|
||||||
|
// down so real bugs surface.
|
||||||
|
if (
|
||||||
|
code === "EPIPE" ||
|
||||||
|
code === "ERR_STREAM_DESTROYED" ||
|
||||||
|
code === "ERR_STREAM_WRITE_AFTER_END" ||
|
||||||
|
code === "ECONNRESET"
|
||||||
|
) {
|
||||||
|
logger.warn(
|
||||||
|
{ error: err },
|
||||||
|
"Unhandled rejection transient stream error — continuing",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.error({ error: err, reason: String(reason) }, "Unhandled rejection");
|
||||||
gracefulShutdown("unhandledRejection");
|
gracefulShutdown("unhandledRejection");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -333,7 +333,15 @@ a=ice-lite
|
|||||||
if (seq) this._sequenceNumber = seq;
|
if (seq) this._sequenceNumber = seq;
|
||||||
if (op === VoiceOpCodes.READY) {
|
if (op === VoiceOpCodes.READY) {
|
||||||
this.handleReady(d);
|
this.handleReady(d);
|
||||||
this.setProtocols().then(() => this.ready?.(this._webRtcWrapper));
|
this.setProtocols()
|
||||||
|
.then(() => this.ready?.(this._webRtcWrapper))
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
// PC can be closed while setProtocols is in flight (stream
|
||||||
|
// teardown) — don't let that become an unhandledRejection.
|
||||||
|
console.log(
|
||||||
|
`[goLive:${this.constructor.name}] setProtocols rejected during teardown: ${err instanceof Error ? err.message : String(err)}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
this.setVideoAttributes(false);
|
this.setVideoAttributes(false);
|
||||||
} else if (op >= 4000) {
|
} else if (op >= 4000) {
|
||||||
console.error(`${this.constructor.name} connection error`, d);
|
console.error(`${this.constructor.name} connection error`, d);
|
||||||
@@ -539,9 +547,18 @@ a=ice-lite
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
// createOffer (binding resolves full SDP incl. candidates after gathering)
|
// createOffer (binding resolves full SDP incl. candidates after gathering)
|
||||||
void webRtcConn.createOffer().then((sdp) => {
|
void webRtcConn
|
||||||
this._webRtcWrapper.onLocalDescription?.(sdp);
|
.createOffer()
|
||||||
});
|
.then((sdp) => {
|
||||||
|
this._webRtcWrapper.onLocalDescription?.(sdp);
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
// PC closed while offer is gathering (stream teardown / reconnect) —
|
||||||
|
// swallow, the reconnect loop will start a fresh offer.
|
||||||
|
console.log(
|
||||||
|
`[goLive:${this.constructor.name}] createOffer rejected: ${err instanceof Error ? err.message : String(err)}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
reconnect();
|
reconnect();
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user