diff --git a/packages/errors/package.json b/packages/errors/package.json index 9b5c9fff0..88e2bacc6 100644 --- a/packages/errors/package.json +++ b/packages/errors/package.json @@ -37,7 +37,6 @@ "devDependencies": { "@types/lodash": "4.17.24", "@types/mocha": "10.0.10", - "@types/uuid": "11.0.0", "c8": "11.0.0", "esbuild": "0.27.3", "lodash": "4.17.23", @@ -45,8 +44,5 @@ "ts-node": "10.9.2", "typescript": "5.9.3" }, - "dependencies": { - "@stdlib/utils-copy": "0.2.3", - "uuid": "13.0.0" - } + "dependencies": {} } diff --git a/packages/errors/src/GhostError.ts b/packages/errors/src/GhostError.ts index c2979422a..d5d638a0f 100644 --- a/packages/errors/src/GhostError.ts +++ b/packages/errors/src/GhostError.ts @@ -1,4 +1,4 @@ -import {v1 as uuidv1} from 'uuid'; +import {randomUUID} from 'crypto'; import {wrapStack} from './wrap-stack'; export interface GhostErrorOptions { @@ -42,7 +42,7 @@ export class GhostError extends Error { this.errorType = 'InternalServerError'; this.level = 'normal'; this.message = 'The server has encountered an error.'; - this.id = uuidv1(); + this.id = randomUUID(); /** * custom overrides diff --git a/packages/errors/src/utils.ts b/packages/errors/src/utils.ts index ee46264f8..2a34cb1cc 100644 --- a/packages/errors/src/utils.ts +++ b/packages/errors/src/utils.ts @@ -1,10 +1,33 @@ -import deepCopy from '@stdlib/utils-copy'; import {GhostError} from './GhostError'; import * as errors from './errors'; // eslint-disable-next-line @typescript-eslint/no-explicit-any type AnyObject = Record +// structuredClone doesn't preserve custom properties on Error subclasses +// (see https://github.com/ungap/structured-clone/issues/12) +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function deepCloneValue(value: any): any { + if (value === null || typeof value !== 'object') { + return value; + } + if (value instanceof Error) { + const clone = Object.create(Object.getPrototypeOf(value)); + for (const key of Object.getOwnPropertyNames(value)) { + clone[key] = deepCloneValue((value as AnyObject)[key]); + } + return clone; + } + if (Array.isArray(value)) { + return value.map(deepCloneValue); + } + const clone: AnyObject = {}; + for (const key of Object.keys(value)) { + clone[key] = deepCloneValue(value[key]); + } + return clone; +} + const errorsWithBase: Record = {...errors, GhostError}; const _private = { @@ -205,11 +228,7 @@ export function prepareStackForUser(error: Error): Error { stackbits.splice(1, 0, `${error.context}`); } - // @NOTE: would be a good idea to swap out the cloning implementation with native - // `structuredClone` one once we use Node v17 or higher. Before making an - // upgrade make sure structuredClone does a full copy of all properties - // present on a custom error (see issue: https://github.com/ungap/structured-clone/issues/12) - const errorClone = deepCopy(error); + const errorClone = deepCloneValue(error); errorClone.stack = stackbits.join('\n'); return errorClone; }; diff --git a/packages/errors/test/utils.test.ts b/packages/errors/test/utils.test.ts index f91ad3b2f..47ad021df 100644 --- a/packages/errors/test/utils.test.ts +++ b/packages/errors/test/utils.test.ts @@ -38,6 +38,20 @@ describe('Error Utils', function () { assert.notEqual(processedError.message, originalError.message); }); + it('deep clones array values in errorDetails', function () { + const items = [{id: 1}, {id: 2}]; + const ghostError = new errors.ValidationError({ + message: 'mistakes were made', + errorDetails: items + }); + + const processedError = utils.prepareStackForUser(ghostError); + + assert.deepEqual(processedError.errorDetails, items); + items[0].id = 999; + assert.equal(processedError.errorDetails[0].id, 1); + }); + it('Preserves the stack trace', function () { const errorCreatingFunction = () => { return new Error('Original error');