Skip to content

Commit

Permalink
feat: replace GraphQLYogaError with GraphQLError (#1473)
Browse files Browse the repository at this point in the history
* Drop 'GraphQLYogaError'

* Fix tests

* ..

* Fix tests

* fixes

* feat: rename error utility file

Co-authored-by: Laurin Quast <laurinquast@googlemail.com>
  • Loading branch information
ardatan and n1ru4l committed Jul 29, 2022
1 parent 92ddbd4 commit c4b3a9c
Show file tree
Hide file tree
Showing 14 changed files with 166 additions and 78 deletions.
6 changes: 6 additions & 0 deletions .changeset/slow-wasps-hide.md
@@ -0,0 +1,6 @@
---
'graphql-yoga': major
---

**BREAKING**: Remove `GraphQLYogaError` in favor of `GraphQLError`
[Check the documentation to see how to use `GraphQLError`](https://www.graphql-yoga.com/docs/guides/error-masking)
23 changes: 11 additions & 12 deletions packages/graphql-yoga/__tests__/node.spec.ts
@@ -1,4 +1,4 @@
import { getIntrospectionQuery } from 'graphql'
import { getIntrospectionQuery, GraphQLError } from 'graphql'
import { useDisableIntrospection } from '@envelop/disable-introspection'
import EventSource from 'eventsource'
import request from 'supertest'
Expand All @@ -7,12 +7,7 @@ import * as fs from 'fs'
import * as path from 'path'
import * as os from 'os'
import * as crypto from 'crypto'
import {
CORSOptions,
createYoga,
GraphQLYogaError,
Plugin,
} from '../src/index.js'
import { CORSOptions, createYoga, Plugin } from '../src/index.js'
import { getCounterValue, schema } from '../test-utils/schema.js'
import { createTestSchema } from './__fixtures__/schema.js'
import { renderGraphiQL } from '@graphql-yoga/render-graphiql'
Expand Down Expand Up @@ -83,7 +78,7 @@ describe('Masked Error Option', () => {
const resolvers = {
Query: {
hello: () => {
throw new GraphQLYogaError('This error never gets masked.')
throw new GraphQLError('This error never gets masked.')
},
hi: () => {
throw new Error('This error will get mask if you enable maskedError.')
Expand Down Expand Up @@ -255,11 +250,11 @@ describe('Context error', () => {
`)
})

it('GraphQLYogaError thrown within context factory with error masking is not masked', async () => {
it('GraphQLError thrown within context factory with error masking is not masked', async () => {
const yoga = createYoga({
logging: false,
context: () => {
throw new GraphQLYogaError('I like turtles')
throw new GraphQLError('I like turtles')
},
})

Expand All @@ -279,11 +274,15 @@ describe('Context error', () => {
`)
})

it('GraphQLYogaError thrown within context factory has error extensions exposed on the response', async () => {
it('GraphQLError thrown within context factory has error extensions exposed on the response', async () => {
const yoga = createYoga({
logging: false,
context: () => {
throw new GraphQLYogaError('I like turtles', { foo: 1 })
throw new GraphQLError('I like turtles', {
extensions: {
foo: 1,
},
})
},
})

Expand Down
2 changes: 1 addition & 1 deletion packages/graphql-yoga/package.json
Expand Up @@ -55,7 +55,7 @@
"dependencies": {
"@graphql-tools/code-file-loader": "^7.3.0",
"@graphql-tools/mock": "^8.7.0",
"@envelop/core": "^2.4.0",
"@envelop/core": "^2.4.1",
"@envelop/parser-cache": "^4.4.0",
"@envelop/validation-cache": "^4.4.0",
"@graphql-typed-document-node/core": "^3.1.1",
Expand Down
@@ -1,4 +1,3 @@
import { EnvelopError } from '@envelop/core'
import { createGraphQLError } from '@graphql-tools/utils'
import { GraphQLError } from 'graphql'

Expand All @@ -12,8 +11,6 @@ declare module 'graphql' {
}
}

export { EnvelopError as GraphQLYogaError }

function isAggregateError(obj: any): obj is AggregateError {
return obj != null && typeof obj === 'object' && 'errors' in obj
}
Expand Down
1 change: 0 additions & 1 deletion packages/graphql-yoga/src/index.ts
Expand Up @@ -10,5 +10,4 @@ export {
shouldRenderGraphiQL,
renderGraphiQL,
} from './plugins/useGraphiQL.js'
export { GraphQLYogaError } from './GraphQLYogaError.js'
export { Plugin } from './plugins/types.js'
51 changes: 22 additions & 29 deletions packages/graphql-yoga/src/server.ts
Expand Up @@ -5,7 +5,6 @@ import {
useMaskedErrors,
UseMaskedErrorsOpts,
useExtendContext,
enableIf,
useLogger,
useSchema,
PromiseOrValue,
Expand All @@ -19,7 +18,6 @@ import {
YogaInitialContext,
FetchAPI,
GraphQLParams,
FetchEvent,
} from './types.js'
import {
OnRequestHook,
Expand Down Expand Up @@ -77,12 +75,13 @@ import {
isPOSTFormUrlEncodedRequest,
parsePOSTFormUrlEncodedRequest,
} from './plugins/requestParser/POSTFormUrlEncoded.js'
import { handleError } from './GraphQLYogaError.js'
import { handleError } from './error.js'
import { useCheckMethodForGraphQL } from './plugins/requestValidation/useCheckMethodForGraphQL.js'
import { useCheckGraphQLQueryParam } from './plugins/requestValidation/useCheckGraphQLQueryParam.js'
import { useHTTPValidationError } from './plugins/requestValidation/useHTTPValidationError.js'
import { usePreventMutationViaGET } from './plugins/requestValidation/usePreventMutationViaGET.js'
import { useUnhandledRoute } from './plugins/useUnhandledRoute.js'
import { formatError } from './utils/formatError.js'

interface OptionsWithPlugins<TContext> {
/**
Expand All @@ -107,7 +106,7 @@ export type YogaServerOptions<
logging?: boolean | YogaLogger
/**
* Prevent leaking unexpected errors to the client. We highly recommend enabling this in production.
* If you throw `GraphQLYogaError`/`EnvelopError` within your GraphQL resolvers then that error will be sent back to the client.
* If you throw `EnvelopError`/`GraphQLError` within your GraphQL resolvers then that error will be sent back to the client.
*
* You can lean more about this here:
* @see https://graphql-yoga.vercel.app/docs/features/error-masking
Expand Down Expand Up @@ -276,32 +275,37 @@ export class YogaServer<
}
: logger

const maskedErrors = options?.maskedErrors ?? true
const maskedErrorsOpts: UseMaskedErrorsOpts | null =
options?.maskedErrors === false
? null
: {
formatError,
...(typeof options?.maskedErrors === 'object'
? options.maskedErrors
: {}),
}

this.graphqlEndpoint = options?.graphqlEndpoint || '/graphql'

this.plugins = [
// Use the schema provided by the user
enableIf(schema != null, useSchema(schema!)),
schema != null && useSchema(schema),
// Performance things
enableIf(options?.parserCache !== false, () =>
options?.parserCache !== false &&
useParserCache(
typeof options?.parserCache === 'object'
? options?.parserCache
: undefined,
),
),
enableIf(options?.validationCache !== false, () =>
options?.validationCache !== false &&
useValidationCache({
cache:
typeof options?.validationCache === 'object'
? options?.validationCache
: undefined,
}),
),
// Log events - useful for debugging purposes
enableIf(
logger !== false,
logger !== false &&
useLogger({
skipIntrospection: true,
logFn: (eventName, events) => {
Expand Down Expand Up @@ -333,9 +337,7 @@ export class YogaServer<
}
},
}),
),
enableIf(
options?.context != null,
options?.context != null &&
useExtendContext(async (initialContext) => {
if (options?.context) {
if (typeof options.context === 'function') {
Expand All @@ -344,23 +346,21 @@ export class YogaServer<
return options.context
}
}),
),
// Middlewares before processing the incoming HTTP request
useHealthCheck({
id: this.id,
logger: this.logger,
healthCheckEndpoint: options?.healthCheckEndpoint,
readinessCheckEndpoint: options?.readinessCheckEndpoint,
}),
enableIf(options?.cors !== false, () => useCORS(options?.cors)),
enableIf(options?.graphiql !== false, () =>
options?.cors !== false && useCORS(options?.cors),
options?.graphiql !== false &&
useGraphiQL({
graphqlEndpoint: this.graphqlEndpoint,
options: options?.graphiql,
render: options?.renderGraphiQL,
logger: this.logger,
}),
),
// Middlewares before the GraphQL execution
useCheckMethodForGraphQL(),
useRequestParser({
Expand All @@ -371,12 +371,12 @@ export class YogaServer<
match: isPOSTJsonRequest,
parse: parsePOSTJsonRequest,
}),
enableIf(options?.multipart !== false, () =>
options?.multipart !== false &&
useRequestParser({
match: isPOSTMultipartRequest,
parse: parsePOSTMultipartRequest,
}),
),

useRequestParser({
match: isPOSTGraphQLStringRequest,
parse: parsePOSTGraphQLStringRequest,
Expand Down Expand Up @@ -406,16 +406,9 @@ export class YogaServer<
useHTTPValidationError(),
// We make sure that the user doesn't send a mutation with GET
usePreventMutationViaGET(),

enableIf(
!!maskedErrors,
useMaskedErrors(
typeof maskedErrors === 'object' ? maskedErrors : undefined,
),
),
maskedErrorsOpts != null && useMaskedErrors(maskedErrorsOpts),
useUnhandledRoute({
graphqlEndpoint: this.graphqlEndpoint,
// TODO: make this a config option
showLandingPage: options?.landingPage ?? true,
}),
]
Expand Down
2 changes: 0 additions & 2 deletions packages/graphql-yoga/src/types.ts
Expand Up @@ -76,8 +76,6 @@ export type GraphQLServerInject<
? { serverContext?: TServerContext }
: { serverContext: TServerContext })

export { EnvelopError as GraphQLYogaError } from '@envelop/core'

declare global {
interface ReadableStream<R = any> {
[Symbol.asyncIterator]: () => AsyncIterator<R>
Expand Down
32 changes: 32 additions & 0 deletions packages/graphql-yoga/src/utils/formatError.ts
@@ -0,0 +1,32 @@
import { FormatErrorHandler } from '@envelop/core'
import { createGraphQLError } from '@graphql-tools/utils'
import { GraphQLError } from 'graphql'

export const formatError: FormatErrorHandler = (err, message, isDev) => {
if (err instanceof GraphQLError) {
if (err.originalError) {
if (err.originalError.name === 'GraphQLError') {
return err
}
// Original error should be removed
const extensions = {
...err.extensions,
}
if (isDev) {
extensions.originalError = {
message: err.originalError.message,
stack: err.originalError.stack,
}
}
return createGraphQLError(message, {
nodes: err.nodes,
source: err.source,
positions: err.positions,
path: err.path,
extensions,
})
}
return err
}
return new GraphQLError(message)
}
54 changes: 41 additions & 13 deletions website/v3/docs/features/error-masking.mdx
Expand Up @@ -168,12 +168,13 @@ This will add a more detailed error with a proper stacktrace to the errors exten
## Exposing expected errors

Sometimes it is feasible to throw errors within your GraphQL resolvers whose message should be send to clients instead of being masked.
This can be achieved by throwing a `GraphQLYogaError` instead of a "normal" [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error).
This can be achieved by throwing a `GraphQLError` instead of a "normal" [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error).

E.g. you might want to throw an error if a resource cannot be found by an ID.

```ts
import { createServer, GraphQLYogaError } from 'graphql-yoga'
import { createServer } from 'graphql-yoga'
import { GraphQLError } from 'graphql'

const users = [
{
Expand Down Expand Up @@ -207,7 +208,7 @@ const yoga = createYoga({
user: async (_, args) => {
const user = users.find((user) => user.id === args.byId)
if (!user) {
throw new GraphQLYogaError(`User with id '${args.byId}' not found.`)
throw new GraphQLError(`User with id '${args.byId}' not found.`)
}

return user
Expand Down Expand Up @@ -256,10 +257,11 @@ query {

Sometimes it is useful to enrich errors with additional information, such as an error code that can be interpreted by the client.

Error extensions can be passed as the second parameter to the `GraphQLYogaError` constructor.
Error extensions can be passed as the second parameter to the `GraphQLError` constructor.

```ts
import { createServer, GraphQLYogaError } from 'graphql-yoga'
import { createServer } from 'graphql-yoga'
import { GraphQLError } from 'graphql'

const users = [
{
Expand Down Expand Up @@ -293,11 +295,13 @@ const yoga = createYoga({
user: async (_, args) => {
const user = users.find((user) => user.id === args.byId)
if (!user) {
throw new GraphQLYogaError(
throw new GraphQLError(
`User with id '${args.byId}' not found.`,
// error extensions
{
code: 'USER_NOT_FOUND',
extensions: {
code: 'USER_NOT_FOUND',
},
},
)
}
Expand Down Expand Up @@ -350,15 +354,39 @@ query {
The extensions are not only limited to a `code` property. Any JSON serializable value can be passed as extensions.

```ts
throw new GraphQLYogaError(
throw new GraphQLError(
`User with id '${args.byId}' not found.`,
// error extensions
{
code: 'USER_NOT_FOUND',
userId: args.byId,
foo: {
some: {
complex: ['structure'],
extensions: {
code: 'USER_NOT_FOUND',
userId: args.byId,
foo: {
some: {
complex: ['structure'],
},
},
},
},
)
```

### Handling HTTP status codes and headers

With `extensions`, you can pass some additional data to decide the status code and the headers of the error response;

```ts
new GraphQLError(
`User with id '${args.byId}' not found.`,
// error extensions
{
extensions: {
http: {
status: 400,
headers: {
'X-Custom-Header': 'some-value',
}
},
},
},
},
Expand Down

0 comments on commit c4b3a9c

Please sign in to comment.