From 652974e23a6c1ae42efaa0fefb3a9ceef86047f7 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Wed, 12 Aug 2026 00:36:53 +0700 Subject: [PATCH] =?UTF-8?q?fix(goLive):=20gateway=20crash=20on=20screensha?= =?UTF-8?q?re=20stop=20=E2=80=94=20unhandledRejection=20during=20teardown?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- services/discord-gateway/src/app/bootstrap.ts | 22 +++++++++++++++- .../src/goLive/BaseMediaConnection.ts | 25 ++++++++++++++++--- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/services/discord-gateway/src/app/bootstrap.ts b/services/discord-gateway/src/app/bootstrap.ts index 224bd10..f366c32 100644 --- a/services/discord-gateway/src/app/bootstrap.ts +++ b/services/discord-gateway/src/app/bootstrap.ts @@ -303,7 +303,27 @@ export async function initializeDiscordGateway() { }); 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"); }); diff --git a/services/discord-gateway/src/goLive/BaseMediaConnection.ts b/services/discord-gateway/src/goLive/BaseMediaConnection.ts index 0f3e9c3..fc1b80c 100644 --- a/services/discord-gateway/src/goLive/BaseMediaConnection.ts +++ b/services/discord-gateway/src/goLive/BaseMediaConnection.ts @@ -333,7 +333,15 @@ a=ice-lite if (seq) this._sequenceNumber = seq; if (op === VoiceOpCodes.READY) { 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); } else if (op >= 4000) { console.error(`${this.constructor.name} connection error`, d); @@ -539,9 +547,18 @@ a=ice-lite }); }; // createOffer (binding resolves full SDP incl. candidates after gathering) - void webRtcConn.createOffer().then((sdp) => { - this._webRtcWrapper.onLocalDescription?.(sdp); - }); + void webRtcConn + .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(); return new Promise((resolve) => {