From 0e85913893d1fc4e48f51c1b9c60ef808e944947 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 26 Aug 2022 10:42:14 +0300 Subject: [PATCH] fix: improve error serialization --- packages/vitest/src/runtime/error.ts | 29 +++++++++++++++++++++---- test/core/test/serialize.test.ts | 32 ++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/packages/vitest/src/runtime/error.ts b/packages/vitest/src/runtime/error.ts index fe0c93628a12..353096c3a917 100644 --- a/packages/vitest/src/runtime/error.ts +++ b/packages/vitest/src/runtime/error.ts @@ -5,6 +5,14 @@ import { deepClone, getType } from '../utils' const OBJECT_PROTO = Object.getPrototypeOf({}) +function getUnserializableMessage(err: unknown) { + if (err instanceof Error) + return `: ${err.message}` + if (typeof err === 'string') + return `: ${err}` + return '' +} + // 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') @@ -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 @@ -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 } @@ -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) } @@ -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 diff --git a/test/core/test/serialize.test.ts b/test/core/test/serialize.test.ts index 87347bd9567b..96b5300e67d1 100644 --- a/test/core/test/serialize.test.ts +++ b/test/core/test/serialize.test.ts @@ -93,4 +93,36 @@ describe('error serialize', () => { toString: 'Function', }) }) + + 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: ': name cannnot be accessed', + }, + ], + constructor: 'Function', + message: 'test', + name: 'Error', + stack: expect.stringContaining('Error: test'), + toString: 'Function', + unserializable: ': I am unserializable', + }) + }) })