mirror of
https://github.com/stablyai/orca.git
synced 2026-10-25 00:03:59 +00:00
fix(crash): contain hostile thrown values in diagnostics
This commit is contained in:
@@ -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(
|
||||
[
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user