fix(gateway): crash safety on screen-share input timeout + proper error serialization
Root cause of "langsung left": YouTube bot-block/403 on u_c1tRmj7E4 (live
stream, LOGIN_REQUIRED) made yt-dlp timeout in resolveInputWithRetry (12s).
The timeout handler did cleanup() (removing once() listeners) THEN
tee.destroy(new Error(...)) — the PassThrough emitted 'error' with NO
listener left → unhandled stream 'error' event → uncaughtException →
gracefulShutdown → bot left voice.
Fix:
- resolveInputWithRetry: tee.destroy() silently after cleanup (error carried
in the rejection only); add permanent no-op tee.on('error') safety.
- prepareStream: output.on('error') no-op so ffmpeg spawn failure before
playStream attaches a demux listener never crashes the gateway.
- bootstrap: serialize uncaughtException/ClientError/DB errors with
{err, errorMsg, stack} (pino only serializes the 'err' magic key — the old
{error: err} key printed {} so crashes were invisible).
This commit is contained in:
@@ -222,7 +222,10 @@ export async function initializeDiscordGateway() {
|
|||||||
await initializeDatabase();
|
await initializeDatabase();
|
||||||
logger.info("PostgreSQL database initialized");
|
logger.info("PostgreSQL database initialized");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error({ error: err }, "Failed to initialize database");
|
logger.error(
|
||||||
|
{ err, errorMsg: err instanceof Error ? err.message : String(err) },
|
||||||
|
"Failed to initialize database",
|
||||||
|
);
|
||||||
throw new DatabaseError(
|
throw new DatabaseError(
|
||||||
`Database initialization failed: ${err instanceof Error ? err.message : String(err)}`,
|
`Database initialization failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||||
);
|
);
|
||||||
@@ -267,7 +270,10 @@ export async function initializeDiscordGateway() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
client.on("error", (err) => {
|
client.on("error", (err) => {
|
||||||
logger.error({ error: err }, "Client error");
|
logger.error(
|
||||||
|
{ err, errorMsg: err instanceof Error ? err.message : String(err) },
|
||||||
|
"Client error",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
process.on("SIGINT", () => {
|
||||||
@@ -298,7 +304,14 @@ export async function initializeDiscordGateway() {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logger.error({ error: err }, "Uncaught exception");
|
logger.error(
|
||||||
|
{
|
||||||
|
err,
|
||||||
|
errorMsg: err instanceof Error ? err.message : String(err),
|
||||||
|
stack: err?.stack,
|
||||||
|
},
|
||||||
|
"Uncaught exception",
|
||||||
|
);
|
||||||
gracefulShutdown("uncaughtException");
|
gracefulShutdown("uncaughtException");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -234,6 +234,10 @@ export function prepareStream(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Safety: proc may error before playStream attaches a demux listener on
|
||||||
|
// `output`. A no-op listener here prevents an unhandled 'error' event
|
||||||
|
// on the PassThrough from crashing the gateway on ffmpeg spawn failure.
|
||||||
|
output.on("error", () => {});
|
||||||
proc.stdout?.pipe(output);
|
proc.stdout?.pipe(output);
|
||||||
proc.stderr?.on("data", () => {
|
proc.stderr?.on("data", () => {
|
||||||
/* swallow ffmpeg stderr */
|
/* swallow ffmpeg stderr */
|
||||||
|
|||||||
@@ -93,11 +93,11 @@ export class ScreenShareController {
|
|||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
cleanup();
|
cleanup();
|
||||||
destroyInput();
|
destroyInput();
|
||||||
tee.destroy(
|
// Listeners were just removed by cleanup() — destroying tee WITH
|
||||||
new Error(
|
// an error would emit "error" on an unlistened PassThrough and
|
||||||
"Screen input produced no data within 12s — merge likely failed",
|
// surface as an unhandled 'error' event (crash). Destroy
|
||||||
),
|
// silently; the error lives in the rejection only.
|
||||||
);
|
tee.destroy();
|
||||||
reject(
|
reject(
|
||||||
new Error(
|
new Error(
|
||||||
"Screen input produced no data within 12s — merge likely failed",
|
"Screen input produced no data within 12s — merge likely failed",
|
||||||
@@ -131,6 +131,11 @@ export class ScreenShareController {
|
|||||||
tee.once("end", onEnd);
|
tee.once("end", onEnd);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Safety net: cleanup() removes the once() listeners on timeout/error,
|
||||||
|
// but a late error event from input.pipe(tee) can still fire on an
|
||||||
|
// unlistened PassThrough and crash the gateway (unhandled 'error').
|
||||||
|
// A permanent no-op listener guarantees the event is always swallowed.
|
||||||
|
tee.on("error", () => {});
|
||||||
// Pass the tee onward — the encoder consumes the same buffered
|
// Pass the tee onward — the encoder consumes the same buffered
|
||||||
// stream, so no data from the merge is lost.
|
// stream, so no data from the merge is lost.
|
||||||
return tee;
|
return tee;
|
||||||
|
|||||||
Reference in New Issue
Block a user