Skip to content

Commit 37ecc67

Browse files
authored
feat: add originalError in dev mode (#1514)
1 parent c6fbb18 commit 37ecc67

File tree

2 files changed

+93
-15
lines changed

2 files changed

+93
-15
lines changed

packages/core/src/plugins/use-masked-errors.ts

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,39 +6,64 @@ export const DEFAULT_ERROR_MESSAGE = 'Unexpected error.';
66
export type MaskErrorFn = (error: unknown, message: string) => Error;
77

88
export type SerializableGraphQLErrorLike = Error & {
9-
name: 'GraphQLError';
10-
toJSON(): { message: string };
9+
toJSON?(): { message: string };
10+
extensions?: Record<string, unknown>;
1111
};
1212

1313
export function isGraphQLError(error: unknown): error is Error & { originalError?: Error } {
1414
return error instanceof Error && error.name === 'GraphQLError';
1515
}
1616

17-
export function createSerializableGraphQLError(message: string): SerializableGraphQLErrorLike {
18-
const error = new Error(message);
17+
function createSerializableGraphQLError(
18+
message: string,
19+
originalError: unknown,
20+
isDev: boolean
21+
): SerializableGraphQLErrorLike {
22+
const error: SerializableGraphQLErrorLike = new Error(message);
1923
error.name = 'GraphQLError';
24+
if (isDev) {
25+
const extensions =
26+
originalError instanceof Error
27+
? { message: originalError.message, stack: originalError.stack }
28+
: { message: String(originalError) };
29+
30+
Object.defineProperty(error, 'extensions', {
31+
get() {
32+
return extensions;
33+
},
34+
});
35+
}
36+
2037
Object.defineProperty(error, 'toJSON', {
2138
value() {
2239
return {
2340
message: error.message,
41+
extensions: error.extensions,
2442
};
2543
},
2644
});
45+
2746
return error as SerializableGraphQLErrorLike;
2847
}
2948

30-
export const defaultMaskErrorFn: MaskErrorFn = (err, message) => {
31-
if (isGraphQLError(err)) {
32-
if (err?.originalError) {
33-
if (isGraphQLError(err.originalError)) {
34-
return err;
49+
export const createDefaultMaskErrorFn =
50+
(isDev: boolean): MaskErrorFn =>
51+
(error, message) => {
52+
if (isGraphQLError(error)) {
53+
if (error?.originalError) {
54+
if (isGraphQLError(error.originalError)) {
55+
return error;
56+
}
57+
return createSerializableGraphQLError(message, error, isDev);
3558
}
36-
return createSerializableGraphQLError(message);
59+
return error;
3760
}
38-
return err;
39-
}
40-
return createSerializableGraphQLError(message);
41-
};
61+
return createSerializableGraphQLError(message, error, isDev);
62+
};
63+
64+
const isDev = globalThis.process?.env?.NODE_ENV === 'development';
65+
66+
export const defaultMaskErrorFn: MaskErrorFn = createDefaultMaskErrorFn(isDev);
4267

4368
export type UseMaskedErrorsOpts = {
4469
/** The function used for identify and mask errors. */

packages/core/test/plugins/use-masked-errors.spec.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import {
55
collectAsyncIteratorValues,
66
createTestkit,
77
} from '@envelop/testing';
8-
import { useMaskedErrors, DEFAULT_ERROR_MESSAGE, MaskErrorFn } from '../../src/plugins/use-masked-errors.js';
8+
import {
9+
useMaskedErrors,
10+
DEFAULT_ERROR_MESSAGE,
11+
MaskErrorFn,
12+
createDefaultMaskErrorFn,
13+
} from '../../src/plugins/use-masked-errors.js';
914
import { useExtendContext } from '@envelop/core';
1015
import { useAuth0 } from '../../../plugins/auth0/src/index.js';
1116
import { GraphQLError } from 'graphql';
@@ -427,4 +432,52 @@ describe('useMaskedErrors', () => {
427432
}
428433
expect.assertions(1);
429434
});
435+
436+
it('should include the original error message stack in the extensions in development mode', async () => {
437+
const schema = makeExecutableSchema({
438+
typeDefs: /* GraphQL */ `
439+
type Query {
440+
foo: String
441+
}
442+
`,
443+
resolvers: {
444+
Query: {
445+
foo: () => {
446+
throw new Error("I'm a teapot");
447+
},
448+
},
449+
},
450+
});
451+
const testInstance = createTestkit([useMaskedErrors({ maskErrorFn: createDefaultMaskErrorFn(true) })], schema);
452+
const result = await testInstance.execute(`query { foo }`, {}, {});
453+
assertSingleExecutionValue(result);
454+
expect(result.errors?.[0].extensions).toEqual({
455+
message: "I'm a teapot",
456+
stack: expect.stringMatching(/^Error: I'm a teapot/),
457+
});
458+
});
459+
460+
it('should include the original thrown thing in the extensions in development mode', async () => {
461+
const schema = makeExecutableSchema({
462+
typeDefs: /* GraphQL */ `
463+
type Query {
464+
foo: String
465+
}
466+
`,
467+
resolvers: {
468+
Query: {
469+
foo: () => {
470+
throw "I'm a teapot";
471+
},
472+
},
473+
},
474+
});
475+
const testInstance = createTestkit([useMaskedErrors({ maskErrorFn: createDefaultMaskErrorFn(true) })], schema);
476+
const result = await testInstance.execute(`query { foo }`, {}, {});
477+
assertSingleExecutionValue(result);
478+
expect(result.errors?.[0].extensions).toEqual({
479+
message: 'Unexpected error value: "I\'m a teapot"',
480+
stack: expect.stringMatching(/NonErrorThrown: Unexpected error value: \"I'm a teapot/),
481+
});
482+
});
430483
});

0 commit comments

Comments
 (0)