Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: improve error serialization #1921

Merged
merged 1 commit into from Aug 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
29 changes: 25 additions & 4 deletions packages/vitest/src/runtime/error.ts
Expand Up @@ -5,6 +5,14 @@ import { deepClone, getType } from '../utils'

const OBJECT_PROTO = Object.getPrototypeOf({})

function getUnserializableMessage(err: unknown) {
if (err instanceof Error)
return `<unserializable>: ${err.message}`
if (typeof err === 'string')
return `<unserializable>: ${err}`
return '<unserializable>'
}

// https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
export function serializeError(val: any, seen = new WeakMap()): any {
if (!val || typeof val === 'string')
Expand All @@ -13,7 +21,7 @@ export function serializeError(val: any, seen = new WeakMap()): any {
return `Function<${val.name}>`
if (typeof val !== 'object')
return val
if (val instanceof Promise || 'then' in val || (val.constructor && val.constructor.prototype === 'AsyncFunction'))
if (val instanceof Promise || (val.constructor && val.constructor.prototype === 'AsyncFunction'))
return 'Promise'
if (typeof Element !== 'undefined' && val instanceof Element)
return val.tagName
Expand All @@ -27,7 +35,12 @@ export function serializeError(val: any, seen = new WeakMap()): any {
const clone: any[] = new Array(val.length)
seen.set(val, clone)
val.forEach((e, i) => {
clone[i] = serializeError(e, seen)
try {
clone[i] = serializeError(e, seen)
}
catch (err) {
clone[i] = getUnserializableMessage(err)
}
})
return clone
}
Expand All @@ -40,8 +53,16 @@ export function serializeError(val: any, seen = new WeakMap()): any {
let obj = val
while (obj && obj !== OBJECT_PROTO) {
Object.getOwnPropertyNames(obj).forEach((key) => {
if (!(key in clone))
if ((key in clone))
return
try {
clone[key] = serializeError(obj[key], seen)
}
catch (err) {
// delete in case it has a setter from prototype that might throw
delete clone[key]
clone[key] = getUnserializableMessage(err)
}
})
obj = Object.getPrototypeOf(obj)
}
Expand All @@ -54,7 +75,7 @@ function normalizeErrorMessage(message: string) {
}

export function processError(err: any) {
if (!err)
if (!err || typeof err !== 'object')
return err
// stack is not serialized in worker communication
// we stringify it first
Expand Down
32 changes: 32 additions & 0 deletions test/core/test/serialize.test.ts
Expand Up @@ -93,4 +93,36 @@ describe('error serialize', () => {
toString: 'Function<toString>',
})
})

it('Should not fail on errored getters/setters', () => {
const error = new Error('test')
Object.defineProperty(error, 'unserializable', {
get() {
throw new Error('I am unserializable')
},
set() {
throw new Error('I am unserializable')
},
})
Object.defineProperty(error, 'array', {
value: [{
get name() {
throw new Error('name cannnot be accessed')
},
}],
})
expect(serializeError(error)).toEqual({
array: [
{
name: '<unserializable>: name cannnot be accessed',
},
],
constructor: 'Function<Error>',
message: 'test',
name: 'Error',
stack: expect.stringContaining('Error: test'),
toString: 'Function<toString>',
unserializable: '<unserializable>: I am unserializable',
})
})
})