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

Drop GraphQLYogaError #1473

Merged
merged 6 commits into from
Jul 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/slow-wasps-hide.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Loading