Skip to content

Commit 6cd09ee

Browse files
committed
fix(core): normalize stale inherited stack headers in finding output
Some libraries (e.g. pdf.js) use prototype-based error constructors where .stack is inherited from a prototype Error, showing a stale header like "Error" instead of the actual .name and .message. Detect this mismatch and replace the first line of .stack with the correct name: message before printing. Only applies to non-Finding errors, since cleanErrorStack already rewrites Finding headers.
1 parent 6af3a28 commit 6cd09ee

2 files changed

Lines changed: 76 additions & 2 deletions

File tree

packages/core/finding.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,53 @@ describe("Finding", () => {
8181
);
8282
expect(lines[3]).toEqual("");
8383
});
84+
85+
it("print error with inherited/stale stack (e.g. pdf.js BaseException)", () => {
86+
const printer = mockPrinter();
87+
// Simulate pdf.js's BaseException pattern: .name and .message are own
88+
// properties, but .stack is inherited from a prototype Error instance.
89+
const proto = new Error();
90+
const error = Object.create(proto) as Error;
91+
Object.defineProperty(error, "message", {
92+
value: "Command token too long: 128",
93+
});
94+
Object.defineProperty(error, "name", {
95+
value: "UnknownErrorException",
96+
});
97+
// error.stack is inherited from proto — stale "Error" header
98+
99+
printFinding(error, printer);
100+
101+
const output = printer.printed();
102+
expect(output).toContain("Uncaught Exception:");
103+
expect(output).toContain(
104+
"UnknownErrorException: Command token too long: 128",
105+
);
106+
expect(output).not.toMatch(/\nError[:\s]*\n/);
107+
});
108+
109+
it("print error without stack shows name: message", () => {
110+
const printer = mockPrinter();
111+
const error = { name: "CustomError", message: "something broke" };
112+
113+
printFinding(error as Error, printer);
114+
115+
const output = printer.printed();
116+
expect(output).toContain("Uncaught Exception:");
117+
expect(output).toContain("CustomError: something broke");
118+
});
119+
120+
it("print duck-typed error without .name falls back to Error", () => {
121+
const printer = mockPrinter();
122+
const error = { message: "oops" };
123+
124+
printFinding(error as Error, printer);
125+
126+
const output = printer.printed();
127+
expect(output).toContain("Uncaught Exception:");
128+
expect(output).toContain("Error: oops");
129+
expect(output).not.toContain("undefined");
130+
});
84131
});
85132

86133
function mockPrinter() {

packages/core/finding.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,17 @@ export function printFinding(
108108
if (isError(error)) {
109109
if (error.stack) {
110110
cleanErrorStack(error);
111-
print(error.stack);
111+
if (error instanceof Finding) {
112+
print(error.stack);
113+
} else {
114+
print(repairStackHeader(error));
115+
}
112116
} else {
113-
print(error.message);
117+
if (error instanceof Finding) {
118+
print(error.message);
119+
} else {
120+
print(`${error.name || "Error"}: ${error.message}`);
121+
}
114122
}
115123
} else if (typeof error === "string" || error instanceof String) {
116124
print(error.toString());
@@ -167,6 +175,25 @@ export function cleanErrorStack(error: unknown): void {
167175
.join("\n");
168176
}
169177

178+
/**
179+
* Fix the first line of error.stack when it doesn't reflect the actual
180+
* .name/.message. This happens with legacy constructor patterns (e.g. pdf.js
181+
* BaseException) where .stack is inherited from a prototype Error and shows
182+
* a stale header from construction time.
183+
*/
184+
function repairStackHeader(error: Error): string {
185+
const stack = error.stack!;
186+
const name = error.name || "Error";
187+
const expectedPrefix = error.message ? `${name}: ${error.message}` : name;
188+
const firstNewline = stack.indexOf("\n");
189+
const firstLine = firstNewline === -1 ? stack : stack.slice(0, firstNewline);
190+
if (firstLine === expectedPrefix) {
191+
return stack;
192+
}
193+
const rest = firstNewline === -1 ? "" : stack.slice(firstNewline);
194+
return expectedPrefix + rest;
195+
}
196+
170197
export function errorName(error: unknown): string {
171198
if (error instanceof Error) {
172199
// error objects

0 commit comments

Comments
 (0)