fix(crash): contain hostile thrown values in diagnostics

This commit is contained in:
Brennan Benson
2026-08-26 16:36:36 -07:00
parent cb92b182ac
commit f51bc90f03
2 changed files with 75 additions and 7 deletions
@@ -2,6 +2,43 @@ import { describe, expect, it } from 'vitest'
import { describeCrashError } from './crash-error-description'
describe('describeCrashError', () => {
it('does not throw when a non-Error value cannot be converted to a string', () => {
const hostile = {
[Symbol.toPrimitive](): never {
throw new Error('conversion failed')
}
}
expect(() => describeCrashError(hostile)).not.toThrow()
expect(describeCrashError(hostile)).toMatchObject({
errorName: 'NonErrorThrown',
errorMessage: '[unprintable thrown value]'
})
})
it('normalizes malformed Error fields before sanitizing them', () => {
const error = new Error('fallback')
Object.defineProperties(error, {
message: { value: Symbol('message') },
name: { value: 42 },
stack: {
get() {
return {
toString(): never {
throw new Error('conversion failed')
}
}
}
}
})
expect(describeCrashError(error)).toMatchObject({
errorName: '42',
errorMessage: 'Symbol(message)'
})
expect(describeCrashError(error)).not.toHaveProperty('errorStack')
})
it('retains sanitized messages and the full sanitized stack alongside a fingerprint', () => {
const error = new Error(
[
+38 -7
View File
@@ -8,21 +8,52 @@ export function describeCrashError(
error: unknown,
componentStack?: string | null
): CrashErrorDescription {
const candidate = error instanceof Error ? error : null
const message = candidate?.message ?? String(error)
let candidate: Error | null = null
try {
candidate = error instanceof Error ? error : null
} catch {
// A revoked proxy can throw while checking its prototype.
}
const message = safeString(
readErrorProperty(candidate, 'message') ?? error,
'[unprintable thrown value]'
)
const rawName = readErrorProperty(candidate, 'name')
const name = rawName == null ? 'NonErrorThrown' : safeString(rawName, 'NonErrorThrown')
const rawStack = readErrorProperty(candidate, 'stack')
const stack = rawStack == null ? '' : safeString(rawStack, '')
return {
errorName: sanitizeCrashReportString(candidate?.name || 'NonErrorThrown', 80),
errorName: sanitizeCrashReportString(name || 'NonErrorThrown', 80),
errorMessage: sanitizeCrashReportString(message),
errorFingerprint: fingerprint(message),
...(candidate?.stack
? { errorStack: sanitizeCrashReportString(candidate.stack, MAX_STACK_CHARS) }
: {}),
...(componentStack?.trim()
...(stack ? { errorStack: sanitizeCrashReportString(stack, MAX_STACK_CHARS) } : {}),
...(typeof componentStack === 'string' && componentStack.trim()
? { componentStack: sanitizeCrashReportString(componentStack.trim(), MAX_STACK_CHARS) }
: {})
}
}
function readErrorProperty(error: Error | null, key: 'message' | 'name' | 'stack'): unknown {
try {
return error?.[key]
} catch {
return undefined
}
}
function safeString(value: unknown, fallback: string): string {
if (typeof value === 'string') {
return value
}
try {
return String(value)
} catch {
return fallback
}
}
function fingerprint(value: string): string {
let hash = 0x811c9dc5
for (let index = 0; index < value.length; index += 1) {