-
-
Notifications
You must be signed in to change notification settings - Fork 114
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: remove handler for validation and parse errors (#1510)
* 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
1 parent
050d317
commit b1bf2e9
Showing
4 changed files
with
670 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 })] | ||
}) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
}, | ||
}; | ||
}, | ||
}; | ||
}; |
Oops, something went wrong.