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

Masked error plugin: Configure custom message used to mask errors #725

Merged
merged 4 commits into from Sep 27, 2021
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
5 changes: 5 additions & 0 deletions .changeset/few-horses-exist.md
@@ -0,0 +1,5 @@
---
'@envelop/core': minor
---

Adds a custom message option used when masking errors
24 changes: 24 additions & 0 deletions packages/core/docs/use-masked-errors.md
Expand Up @@ -36,3 +36,27 @@ const getEnveloped = envelop({
plugins: [useSchema(schema), useMaskedErrors()],
});
```

You may customize the default error message `Unexpected error.` with your own `errorMessage`:

```ts
const getEnveloped = envelop({
plugins: [useSchema(schema), useMaskedErrors({ errorMessage: 'Something went wrong.' })],
});
```

Or provide a custom formatter when masking the output:

```ts
export const customFormatError: FormatErrorHandler = (err: any) => {
if (err.originalError && err.originalError instanceof EnvelopError === false) {
return new GraphQLError('Sorry, something went wrong.');
}

return err;
};

const getEnveloped = envelop({
plugins: [useSchema(schema), useMaskedErrors({ formatError: customFormatError })],
});
```
22 changes: 13 additions & 9 deletions packages/core/src/plugins/use-masked-errors.ts
@@ -1,44 +1,48 @@
import { handleStreamOrSingleExecutionResult, Plugin } from '@envelop/types';
import { ExecutionResult, GraphQLError } from 'graphql';

export const DEFAULT_ERROR_MESSAGE = 'Unexpected error.';

export class EnvelopError extends GraphQLError {
constructor(message: string, extensions?: Record<string, any>) {
super(message, undefined, undefined, undefined, undefined, undefined, extensions);
}
}

export type FormatErrorHandler = (error: GraphQLError | unknown) => GraphQLError;
export type FormatErrorHandler = (error: GraphQLError | unknown, message: string) => GraphQLError;

export const formatError: FormatErrorHandler = err => {
export const formatError: FormatErrorHandler = (err, message) => {
if (err instanceof GraphQLError) {
if (err.originalError && err.originalError instanceof EnvelopError === false) {
return new GraphQLError('Unexpected error.', err.nodes, err.source, err.positions, err.path, undefined);
return new GraphQLError(message, err.nodes, err.source, err.positions, err.path, undefined);
}
return err;
}
return new GraphQLError('Unexpected error.');
return new GraphQLError(message);
};

export type UseMaskedErrorsOpts = {
formatError?: FormatErrorHandler;
errorMessage?: string;
};

const makeHandleResult =
(format: FormatErrorHandler) =>
(format: FormatErrorHandler, message: string) =>
({ result, setResult }: { result: ExecutionResult; setResult: (result: ExecutionResult) => void }) => {
if (result.errors != null) {
setResult({ ...result, errors: result.errors.map(error => format(error)) });
setResult({ ...result, errors: result.errors.map(error => format(error, message)) });
}
};

export const useMaskedErrors = (opts?: UseMaskedErrorsOpts): Plugin => {
const format = opts?.formatError ?? formatError;
const handleResult = makeHandleResult(format);
const message = opts?.errorMessage || DEFAULT_ERROR_MESSAGE;
const handleResult = makeHandleResult(format, message);

return {
onPluginInit(context) {
context.registerContextErrorHandler(({ error, setError }) => {
setError(formatError(error));
setError(formatError(error, message));
});
},
onExecute() {
Expand All @@ -54,7 +58,7 @@ export const useMaskedErrors = (opts?: UseMaskedErrorsOpts): Plugin => {
return handleStreamOrSingleExecutionResult(payload, handleResult);
},
onSubscribeError({ error, setError }) {
setError(formatError(error));
setError(formatError(error, message));
},
};
},
Expand Down
68 changes: 62 additions & 6 deletions packages/core/test/plugins/use-masked-errors.spec.ts
Expand Up @@ -5,7 +5,7 @@ import {
collectAsyncIteratorValues,
createTestkit,
} from '@envelop/testing';
import { EnvelopError, useMaskedErrors } from '../../src/plugins/use-masked-errors';
import { EnvelopError, useMaskedErrors, DEFAULT_ERROR_MESSAGE } from '../../src/plugins/use-masked-errors';
import { useExtendContext } from '@envelop/core';

describe('useMaskedErrors', () => {
Expand Down Expand Up @@ -92,7 +92,7 @@ describe('useMaskedErrors', () => {
expect(result.errors).toBeDefined();
expect(result.errors).toHaveLength(1);
const [error] = result.errors!;
expect(error.message).toEqual('Unexpected error.');
expect(error.message).toEqual(DEFAULT_ERROR_MESSAGE);
});

it('Should mask unexpected errors', async () => {
Expand Down Expand Up @@ -128,6 +128,23 @@ describe('useMaskedErrors', () => {
});
});

it('Should properly mask context creation errors with a custom error message', async () => {
expect.assertions(1);
const testInstance = createTestkit(
[
useExtendContext((): {} => {
throw new Error('No context for you!');
}),
useMaskedErrors({ errorMessage: 'My Custom Error Message.' }),
],
schema
);
try {
await testInstance.execute(`query { secretWithExtensions }`);
} catch (err) {
expect(err).toMatchInlineSnapshot(`[GraphQLError: My Custom Error Message.]`);
}
});
it('Should properly mask context creation errors', async () => {
expect.assertions(1);
const testInstance = createTestkit(
Expand All @@ -142,7 +159,7 @@ describe('useMaskedErrors', () => {
try {
await testInstance.execute(`query { secretWithExtensions }`);
} catch (err) {
expect(err).toMatchInlineSnapshot(`[GraphQLError: Unexpected error.]`);
expect(err).toMatchInlineSnapshot(`[GraphQLError: ${DEFAULT_ERROR_MESSAGE}]`);
}
});

Expand Down Expand Up @@ -170,7 +187,19 @@ describe('useMaskedErrors', () => {
expect(result.errors).toBeDefined();
expect(result.errors).toMatchInlineSnapshot(`
Array [
[GraphQLError: Unexpected error.],
[GraphQLError: ${DEFAULT_ERROR_MESSAGE}],
]
`);
});

it('Should mask subscribe (sync/promise) subscription errors with a custom error message', async () => {
const testInstance = createTestkit([useMaskedErrors({ errorMessage: 'My Custom subscription error message.' })], schema);
const result = await testInstance.execute(`subscription { instantError }`);
assertSingleExecutionValue(result);
expect(result.errors).toBeDefined();
expect(result.errors).toMatchInlineSnapshot(`
Array [
[GraphQLError: My Custom subscription error message.],
]
`);
});
Expand All @@ -195,9 +224,22 @@ Array [
try {
await collectAsyncIteratorValues(resultStream);
} catch (err) {
expect(err).toMatchInlineSnapshot(`[GraphQLError: Unexpected error.]`);
expect(err).toMatchInlineSnapshot(`[GraphQLError: ${DEFAULT_ERROR_MESSAGE}]`);
}
});

it('Should mask subscribe (AsyncIterable) subscription errors with a custom error message', async () => {
expect.assertions(1);
const testInstance = createTestkit([useMaskedErrors({ errorMessage: 'My AsyncIterable Custom Error Message.' })], schema);
const resultStream = await testInstance.execute(`subscription { streamError }`);
assertStreamExecutionValue(resultStream);
try {
await collectAsyncIteratorValues(resultStream);
} catch (err) {
expect(err).toMatchInlineSnapshot(`[GraphQLError: My AsyncIterable Custom Error Message.]`);
}
});

it('Should not mask subscribe (AsyncIterable) subscription envelop errors', async () => {
const testInstance = createTestkit([useMaskedErrors()], schema);
const resultStream = await testInstance.execute(`subscription { streamEnvelopError }`);
Expand All @@ -219,7 +261,21 @@ Array [
expect(result.errors).toBeDefined();
expect(result.errors).toMatchInlineSnapshot(`
Array [
[GraphQLError: Unexpected error.],
[GraphQLError: ${DEFAULT_ERROR_MESSAGE}],
]
`);
});
it('Should mask resolve subscription errors with a custom error message', async () => {
const testInstance = createTestkit([useMaskedErrors({ errorMessage: 'Custom resolve subscription errors.' })], schema);
const resultStream = await testInstance.execute(`subscription { streamResolveError }`);
assertStreamExecutionValue(resultStream);
const allResults = await collectAsyncIteratorValues(resultStream);
expect(allResults).toHaveLength(1);
const [result] = allResults;
expect(result.errors).toBeDefined();
expect(result.errors).toMatchInlineSnapshot(`
Array [
[GraphQLError: Custom resolve subscription errors.],
]
`);
});
Expand Down