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) => {