diff --git a/packages/core/src/plugins/use-masked-errors.ts b/packages/core/src/plugins/use-masked-errors.ts index 8782f4cc9..aa3c72a77 100644 --- a/packages/core/src/plugins/use-masked-errors.ts +++ b/packages/core/src/plugins/use-masked-errors.ts @@ -6,39 +6,64 @@ export const DEFAULT_ERROR_MESSAGE = 'Unexpected error.'; export type MaskErrorFn = (error: unknown, message: string) => Error; export type SerializableGraphQLErrorLike = Error & { - name: 'GraphQLError'; - toJSON(): { message: string }; + toJSON?(): { message: string }; + extensions?: Record; }; export function isGraphQLError(error: unknown): error is Error & { originalError?: Error } { return error instanceof Error && error.name === 'GraphQLError'; } -export function createSerializableGraphQLError(message: string): SerializableGraphQLErrorLike { - const error = new Error(message); +function createSerializableGraphQLError( + message: string, + originalError: unknown, + isDev: boolean +): SerializableGraphQLErrorLike { + const error: SerializableGraphQLErrorLike = new Error(message); error.name = 'GraphQLError'; + if (isDev) { + const extensions = + originalError instanceof Error + ? { message: originalError.message, stack: originalError.stack } + : { message: String(originalError) }; + + Object.defineProperty(error, 'extensions', { + get() { + return extensions; + }, + }); + } + Object.defineProperty(error, 'toJSON', { value() { return { message: error.message, + extensions: error.extensions, }; }, }); + return error as SerializableGraphQLErrorLike; } -export const defaultMaskErrorFn: MaskErrorFn = (err, message) => { - if (isGraphQLError(err)) { - if (err?.originalError) { - if (isGraphQLError(err.originalError)) { - return err; +export const createDefaultMaskErrorFn = + (isDev: boolean): MaskErrorFn => + (error, message) => { + if (isGraphQLError(error)) { + if (error?.originalError) { + if (isGraphQLError(error.originalError)) { + return error; + } + return createSerializableGraphQLError(message, error, isDev); } - return createSerializableGraphQLError(message); + return error; } - return err; - } - return createSerializableGraphQLError(message); -}; + return createSerializableGraphQLError(message, error, isDev); + }; + +const isDev = globalThis.process?.env?.NODE_ENV === 'development'; + +export const defaultMaskErrorFn: MaskErrorFn = createDefaultMaskErrorFn(isDev); export type UseMaskedErrorsOpts = { /** The function used for identify and mask errors. */ diff --git a/packages/core/test/plugins/use-masked-errors.spec.ts b/packages/core/test/plugins/use-masked-errors.spec.ts index bee5ba2c6..80f9bd145 100644 --- a/packages/core/test/plugins/use-masked-errors.spec.ts +++ b/packages/core/test/plugins/use-masked-errors.spec.ts @@ -5,7 +5,12 @@ import { collectAsyncIteratorValues, createTestkit, } from '@envelop/testing'; -import { useMaskedErrors, DEFAULT_ERROR_MESSAGE, MaskErrorFn } from '../../src/plugins/use-masked-errors.js'; +import { + useMaskedErrors, + DEFAULT_ERROR_MESSAGE, + MaskErrorFn, + createDefaultMaskErrorFn, +} from '../../src/plugins/use-masked-errors.js'; import { useExtendContext } from '@envelop/core'; import { useAuth0 } from '../../../plugins/auth0/src/index.js'; import { GraphQLError } from 'graphql'; @@ -427,4 +432,52 @@ describe('useMaskedErrors', () => { } expect.assertions(1); }); + + it('should include the original error message stack in the extensions in development mode', async () => { + const schema = makeExecutableSchema({ + typeDefs: /* GraphQL */ ` + type Query { + foo: String + } + `, + resolvers: { + Query: { + foo: () => { + throw new Error("I'm a teapot"); + }, + }, + }, + }); + const testInstance = createTestkit([useMaskedErrors({ maskErrorFn: createDefaultMaskErrorFn(true) })], schema); + const result = await testInstance.execute(`query { foo }`, {}, {}); + assertSingleExecutionValue(result); + expect(result.errors?.[0].extensions).toEqual({ + message: "I'm a teapot", + stack: expect.stringMatching(/^Error: I'm a teapot/), + }); + }); + + it('should include the original thrown thing in the extensions in development mode', async () => { + const schema = makeExecutableSchema({ + typeDefs: /* GraphQL */ ` + type Query { + foo: String + } + `, + resolvers: { + Query: { + foo: () => { + throw "I'm a teapot"; + }, + }, + }, + }); + const testInstance = createTestkit([useMaskedErrors({ maskErrorFn: createDefaultMaskErrorFn(true) })], schema); + const result = await testInstance.execute(`query { foo }`, {}, {}); + assertSingleExecutionValue(result); + expect(result.errors?.[0].extensions).toEqual({ + message: 'Unexpected error value: "I\'m a teapot"', + stack: expect.stringMatching(/NonErrorThrown: Unexpected error value: \"I'm a teapot/), + }); + }); });