fix(discord): preserve stack hints for empty gateway type errors

This commit is contained in:
Peter Steinberger
2026-04-06 16:20:25 +01:00
parent 90d246959b
commit d5bfc79112
2 changed files with 49 additions and 13 deletions

View File

@@ -8,6 +8,8 @@ import {
describe("classifyDiscordGatewayEvent", () => {
it("maps current Carbon gateway errors onto domain events", () => {
const transientTypeError = new TypeError();
transientTypeError.stack = "TypeError\n at gatewayCrash (discord-gateway.js:12:34)";
const reconnectEvent = classifyDiscordGatewayEvent({
err: new Error("Max reconnect attempts (0) reached after close code 1006"),
isDisallowedIntentsError: () => false,
@@ -21,7 +23,7 @@ describe("classifyDiscordGatewayEvent", () => {
isDisallowedIntentsError: (err) => String(err).includes("4014"),
});
const transientEvent = classifyDiscordGatewayEvent({
err: new TypeError(),
err: transientTypeError,
isDisallowedIntentsError: () => false,
});
@@ -30,20 +32,24 @@ describe("classifyDiscordGatewayEvent", () => {
expect(fatalEvent.type).toBe("fatal");
expect(disallowedEvent.type).toBe("disallowed-intents");
expect(transientEvent.type).toBe("fatal");
expect(transientEvent.message).toBe("TypeError");
expect(transientEvent.message).toBe("TypeError @ gatewayCrash (discord-gateway.js:12:34)");
expect(transientEvent.shouldStopLifecycle).toBe(true);
});
it("wraps fatal lifecycle stops with discord-specific context", () => {
const transientTypeError = new TypeError();
transientTypeError.stack = "TypeError\n at gatewayCrash (discord-gateway.js:12:34)";
const event = classifyDiscordGatewayEvent({
err: new TypeError(),
err: transientTypeError,
isDisallowedIntentsError: () => false,
});
const wrapped = new DiscordGatewayLifecycleError(event);
expect(wrapped.name).toBe("DiscordGatewayLifecycleError");
expect(wrapped.message).toBe("discord gateway fatal: TypeError");
expect(wrapped.message).toBe(
"discord gateway fatal: TypeError @ gatewayCrash (discord-gateway.js:12:34)",
);
expect(wrapped.eventType).toBe("fatal");
expect(wrapped.cause).toBeInstanceOf(TypeError);
});
@@ -127,12 +133,18 @@ describe("createDiscordGatewaySupervisor", () => {
});
supervisor.dispose();
emitter.emit("error", new TypeError());
emitter.emit("error", new TypeError());
const first = new TypeError();
first.stack = "TypeError\n at gatewayCrash (discord-gateway.js:12:34)";
const second = new TypeError();
second.stack = "TypeError\n at gatewayCrash (discord-gateway.js:12:34)";
emitter.emit("error", first);
emitter.emit("error", second);
expect(runtime.error).toHaveBeenCalledTimes(1);
expect(runtime.error).toHaveBeenCalledWith(
expect.stringContaining("suppressed late gateway fatal error after dispose: TypeError"),
expect.stringContaining(
"suppressed late gateway fatal error after dispose: TypeError @ gatewayCrash (discord-gateway.js:12:34)",
),
);
});
});

View File

@@ -41,16 +41,40 @@ export type DiscordGatewaySupervisor = {
type GatewaySupervisorPhase = "active" | "buffering" | "disposed" | "teardown";
function readFirstStackFrame(err: Error): string | undefined {
const stack = err.stack;
if (!stack) {
return undefined;
}
const frame = stack
.split("\n")
.slice(1)
.map((line) => line.trim())
.find(Boolean);
return frame ? frame.replace(/^at\s+/, "") : undefined;
}
function formatDiscordGatewayErrorMessage(err: unknown): string {
if (!(err instanceof Error)) {
return formatErrorMessage(err);
}
if (err.message) {
const detail = formatErrorMessage(err);
return err.name ? `${err.name}: ${detail}` : detail;
}
const detail = formatErrorMessage(err);
const firstFrame = readFirstStackFrame(err);
if (firstFrame && detail === (err.name || "Error")) {
return `${detail} @ ${firstFrame}`;
}
return detail;
}
export function classifyDiscordGatewayEvent(params: {
err: unknown;
isDisallowedIntentsError: (err: unknown) => boolean;
}): DiscordGatewayEvent {
const message =
params.err instanceof Error
? params.err.message
? `${params.err.name}: ${params.err.message}`
: params.err.name || "Error"
: formatErrorMessage(params.err);
const message = formatDiscordGatewayErrorMessage(params.err);
if (params.isDisallowedIntentsError(params.err)) {
return {
type: "disallowed-intents",