Skip to content

Commit

Permalink
feat: remove handler for validation and parse errors (#1510)
Browse files Browse the repository at this point in the history
* feat: remove hanlder for validation and parse errors

* tests

* make it work

* Add docs

* Fix serialization issue

* Go

* ..

* update docs

* update test

* make it work

* feat: add originalError in dev mode (#1514)

* remove Fn appendix

* name graphql error

* make ts happy

* make toJSON required

Co-authored-by: Arda TANRIKULU <ardatanrikulu@gmail.com>
Co-authored-by: Laurin Quast <laurinquast@googlemail.com>
  • Loading branch information
3 people committed Sep 8, 2022
1 parent 050d317 commit b1bf2e9
Show file tree
Hide file tree
Showing 4 changed files with 670 additions and 0 deletions.
10 changes: 10 additions & 0 deletions .changeset/rude-cats-peel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@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
Rename `formatError` function option to `maskErrorFn`
64 changes: 64 additions & 0 deletions packages/core/docs/use-masked-errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#### `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
import { isGraphQLError, MaskErrorFn } from '@envelop/core'

export const customFormatError: MaskErrorFn = err => {
if (isGraphQLError(err)) {
return new GraphQLError('Sorry, something went wrong.')
}

return err
}

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

export const DEFAULT_ERROR_MESSAGE = 'Unexpected error.';

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

export type SerializableGraphQLErrorLike = Error & {
name: 'GraphQLError';
toJSON(): { message: string };
extensions?: Record<string, unknown>;
};

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

function createSerializableGraphQLError(
message: string,
originalError: unknown,
isDev: boolean
): SerializableGraphQLErrorLike {
const error = new Error(message) as SerializableGraphQLErrorLike;
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 createDefaultMaskError =
(isDev: boolean): MaskError =>
(error, message) => {
if (isGraphQLError(error)) {
if (error?.originalError) {
if (isGraphQLError(error.originalError)) {
return error;
}
return createSerializableGraphQLError(message, error, isDev);
}
return error;
}
return createSerializableGraphQLError(message, error, isDev);
};

const isDev = globalThis.process?.env?.NODE_ENV === 'development';

export const defaultMaskError: MaskError = createDefaultMaskError(isDev);

export type UseMaskedErrorsOpts = {
/** The function used for identify and mask errors. */
maskError?: MaskError;
/** The error message that shall be used for masked errors. */
errorMessage?: string;
};

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

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

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

0 comments on commit b1bf2e9

Please sign in to comment.