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

feat: remove handler for validation and parse errors #1510

8 changes: 8 additions & 0 deletions .changeset/rude-cats-peel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@envelop/core': major
---

Remove `handleValidationErrors` and `handleParseErrors` options from `useMaskedErrors`.

> ONLY masking validation errors OR ONLY disabling introspection errors does not make sense, as both can be abused for reverse-engineering the GraphQL schema (see https://github.com/nikitastupin/clairvoyance for reverse-engineering the schema based on validation error suggestions).
> https://github.com/n1ru4l/envelop/issues/1482#issue-1340015060
62 changes: 62 additions & 0 deletions packages/core/docs/use-masked-errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#### `useMaskedErrors`

Prevent unexpected error messages from leaking to the GraphQL clients.

```ts
import { envelop, useSchema, useMaskedErrors } from '@envelop/core'
import { makeExecutableSchema, GraphQLError } from 'graphql'

const schema = makeExecutableSchema({
typeDefs: /* GraphQL */ `
type Query {
something: String!
somethingElse: String!
somethingSpecial: String!
}
`,
resolvers: {
Query: {
something: () => {
throw new GraphQLError('Error that is propagated to the clients.')
},
somethingElse: () => {
throw new Error("Unsafe error that will be masked as 'Unexpected Error.'.")
},
somethingSpecial: () => {
throw new GraphQLError('The error will have an extensions field.', {
code: 'ERR_CODE',
randomNumber: 123
})
}
}
}
})

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 => {
if (err.originalError) {
return new GraphQLError('Sorry, something went wrong.')
}

return err
}

const getEnveloped = envelop({
plugins: [useSchema(schema), useMaskedErrors({ formatError: customFormatError })]
})
```
87 changes: 87 additions & 0 deletions packages/core/src/plugins/use-masked-errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Plugin, ExecutionResult } from '@envelop/types';
import { handleStreamOrSingleExecutionResult } from '../utils.js';

export const DEFAULT_ERROR_MESSAGE = 'Unexpected error.';

export type MaskErrorFn = (error: unknown, message: string) => Error;

export type SerializableGraphQLErrorLike = Error & {
name: 'GraphQLError';
toJSON(): { message: string };
saihaj marked this conversation as resolved.
Show resolved Hide resolved
};

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);
error.name = 'GraphQLError';
Object.defineProperty(error, 'toJSON', {
value() {
return {
message: error.message,
};
},
});
return error as SerializableGraphQLErrorLike;
}

export const defaultMaskErrorFn: MaskErrorFn = (err, message) => {
if (isGraphQLError(err)) {
if (err?.originalError) {
if (isGraphQLError(err.originalError)) {
return err;
}
return createSerializableGraphQLError(message);
}
return err;
}
return createSerializableGraphQLError(message);
};
saihaj marked this conversation as resolved.
Show resolved Hide resolved

export type UseMaskedErrorsOpts = {
/** The function used for identify and mask errors. */
maskErrorFn?: MaskErrorFn;
saihaj marked this conversation as resolved.
Show resolved Hide resolved
/** The error message that shall be used for masked errors. */
errorMessage?: string;
};

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

export const useMaskedErrors = (opts?: UseMaskedErrorsOpts): Plugin => {
const maskErrorFn = opts?.maskErrorFn ?? defaultMaskErrorFn;
const message = opts?.errorMessage || DEFAULT_ERROR_MESSAGE;
saihaj marked this conversation as resolved.
Show resolved Hide resolved
const handleResult = makeHandleResult(maskErrorFn, message);

return {
onPluginInit(context) {
context.registerContextErrorHandler(({ error, setError }) => {
setError(maskErrorFn(error, message));
});
},
onExecute() {
return {
onExecuteDone(payload) {
return handleStreamOrSingleExecutionResult(payload, handleResult);
},
};
},
onSubscribe() {
return {
onSubscribeResult(payload) {
return handleStreamOrSingleExecutionResult(payload, handleResult);
},
onSubscribeError({ error, setError }) {
setError(maskErrorFn(error, message));
},
};
},
};
};