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 })]
})
```
65 changes: 65 additions & 0 deletions packages/core/src/plugins/use-masked-errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Plugin, ExecutionResult } from '@envelop/types';
import { handleStreamOrSingleExecutionResult } from '../utils.js';

export const DEFAULT_ERROR_MESSAGE = 'Unexpected error.';

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

export const formatError: FormatErrorHandler = (err, message) => {
if (err?.name === 'GraphQLError') {
if (err?.originalError) {
if (err.originalError.name === 'GraphQLError') {
return err as Error;
}
return new Error(message);
}
return err as Error;
}
return new Error(message);
};
saihaj marked this conversation as resolved.
Show resolved Hide resolved

export type UseMaskedErrorsOpts = {
/** The function used for format/identify errors. */
formatError?: FormatErrorHandler;
/** The error message that shall be used for masked errors. */
errorMessage?: string;
};

const makeHandleResult =
(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, message)) });
}
};

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

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