Skip to content

Commit

Permalink
feat: export the mask error function + gateway notes (#2364)
Browse files Browse the repository at this point in the history
Co-authored-by: Arda TANRIKULU <ardatanrikulu@gmail.com>
  • Loading branch information
n1ru4l and ardatan committed Jan 31, 2023
1 parent 766118f commit 03597a5
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 10 deletions.
21 changes: 21 additions & 0 deletions .changeset/old-spiders-perform.md
@@ -0,0 +1,21 @@
---
'graphql-yoga': minor
---

export the yoga default format error function.

```ts
import { maskError, createYoga } from 'graphql-yoga'

const yoga = createYoga({
maskedErrors: {
maskError(error, message, isDev) {
if (error?.extensions?.code === 'DOWNSTREAM_SERVICE_ERROR') {
return error
}

return maskError(error, message, isDev)
}
}
})
```
Expand Up @@ -51,4 +51,27 @@ describe('apollo-federation example integration', () => {
},
})
})

it('pass through errors', async () => {
const response = await fetch(
`http://localhost:${gatewayPort}/graphql?query=query{throw}`,
)
const body = await response.json()
expect(body).toMatchInlineSnapshot(`
{
"data": {
"throw": null,
},
"errors": [
{
"extensions": {
"code": "DOWNSTREAM_SERVICE_ERROR",
"serviceName": "accounts",
},
"message": "This should throw.",
},
],
}
`)
})
})
15 changes: 12 additions & 3 deletions examples/apollo-federation/gateway/gateway.js
@@ -1,9 +1,9 @@
/* eslint-disable */
const { createYoga } = require('graphql-yoga')
const { createYoga, maskError } = require('graphql-yoga')
const { ApolloGateway, RemoteGraphQLDataSource } = require('@apollo/gateway')
const { useApolloFederation } = require('@envelop/apollo-federation')

export async function gateway(config) {
module.exports.gateway = async function gateway(config) {
// Initialize the gateway
const gateway = new ApolloGateway(config)

Expand All @@ -16,6 +16,15 @@ export async function gateway(config) {
gateway,
}),
],
maskedErrors: {
maskError(error, message, isDev) {
// Note: it seems like the "useApolloFederation" plugin should do this by default?
if (error?.extensions?.code === 'DOWNSTREAM_SERVICE_ERROR') {
return error
}
return maskError(error, message, isDev)
},
},
})

return yoga
Expand All @@ -27,7 +36,7 @@ export async function gateway(config) {
* By default Yoga uses `application/graphql-response+json` content type as per the GraphQL over HTTP spec
* https://github.com/apollographql/federation/issues/2161
*/
export class DataSource extends RemoteGraphQLDataSource {
module.exports.DataSource = class DataSource extends RemoteGraphQLDataSource {
willSendRequest({ request }) {
request.http.headers.set('accept', 'application/json')
}
Expand Down
8 changes: 6 additions & 2 deletions examples/apollo-federation/service/yoga.js
@@ -1,11 +1,12 @@
/* eslint-disable */
const { parse } = require('graphql')
const { parse, GraphQLError } = require('graphql')
const { buildSubgraphSchema } = require('@apollo/subgraph')
const { createYoga } = require('graphql-yoga')

const typeDefs = parse(/* GraphQL */ `
type Query {
me: User
throw: String
}
type User @key(fields: "id") {
Expand All @@ -19,6 +20,9 @@ const resolvers = {
me() {
return { id: '1', username: '@ava' }
},
throw() {
throw new GraphQLError('This should throw.')
},
},
User: {
__resolveReference(user, { fetchUserById }) {
Expand All @@ -27,6 +31,6 @@ const resolvers = {
},
}

export const yoga = createYoga({
module.exports.yoga = createYoga({
schema: buildSubgraphSchema([{ typeDefs, resolvers }]),
})
1 change: 1 addition & 0 deletions packages/graphql-yoga/src/index.ts
Expand Up @@ -10,6 +10,7 @@ export * from './schema.js'
export * from './server.js'
export * from './subscription.js'
export * from './types.js'
export { maskError } from './utils/mask-error.js'
export type {
// Handy type utils
Maybe,
Expand Down
8 changes: 4 additions & 4 deletions packages/graphql-yoga/src/server.ts
Expand Up @@ -70,7 +70,7 @@ import {
YogaInitialContext,
YogaMaskedErrorOpts,
} from './types.js'
import { yogaDefaultFormatError } from './utils/yoga-default-format-error.js'
import { maskError } from './utils/mask-error.js'

/**
* Configuration options for the server
Expand Down Expand Up @@ -219,7 +219,7 @@ export class YogaServer<
}
}

const logger = options?.logging != null ? options.logging : true
const logger = options?.logging == null ? true : options.logging
this.logger =
typeof logger === 'boolean'
? logger === true
Expand All @@ -232,7 +232,7 @@ export class YogaServer<
const maskErrorFn: MaskError =
(typeof options?.maskedErrors === 'object' &&
options.maskedErrors.maskError) ||
yogaDefaultFormatError
maskError

const maskedErrorSet = new WeakSet()

Expand Down Expand Up @@ -265,7 +265,7 @@ export class YogaServer<
}

const maskedErrors =
this.maskedErrorsOpts != null ? this.maskedErrorsOpts : null
this.maskedErrorsOpts == null ? null : this.maskedErrorsOpts

let batchingLimit = 0
if (options?.batching) {
Expand Down
Expand Up @@ -4,7 +4,7 @@ import { createGraphQLError } from '@graphql-tools/utils'
import { isGraphQLError } from '../error.js'
import { MaskError } from '../types.js'

export const yogaDefaultFormatError: MaskError = (
export const maskError: MaskError = (
error: unknown,
message: string,
isDev = globalThis.process?.env?.NODE_ENV === 'development',
Expand Down
25 changes: 25 additions & 0 deletions website/src/pages/docs/features/apollo-federation.mdx
Expand Up @@ -68,6 +68,31 @@ server.listen(4000, () => {
})
```

### Handling Subgraph Errors

By default, GraphQL Yoga masks any unexpected GraphQL Errors. This is done to prevent leaking internal errors to the client.
If you know that your subgraph is safe and you want to expose the errors to the client, you can customize the error masking bahviour.

[Learn more about error masking](/docs/features/error-masking)

```ts filename="Expose subgraph errors to the client"
import { maskError, createYoga } from 'graphql-yoga'
import { schema } from './schema.js'

const yoga = createYoga({
schema,
maskedErrors: {
maskError(error, message, isDev) {
if (error?.extensions?.code === 'DOWNSTREAM_SERVICE_ERROR') {
return error
}

return maskError(error, message, isDev)
}
}
})
```

## Federation Service

You don't need any extra plugins for Yoga for Federation Service.
Expand Down
25 changes: 25 additions & 0 deletions website/src/pages/docs/features/error-masking.mdx
Expand Up @@ -381,3 +381,28 @@ query {
"data": null
}
```

## Customize error masking

For advanced use-cases e.g. when using GraphQL Yoga as a Gateway, you might to automatically expose errors that are resolved from the subgraphs as you asume these already take care of hiding sensitive information.
In this case, you can customize the error masking behavior by providing a custom `maskError` function.

Use the default `maskError` function as a fallback for all other errors.

```ts filename="Custom Mask Error Usage"
import { maskError, createYoga } from 'graphql-yoga'
import { schema } from './schema.js'

const yoga = createYoga({
schema,
maskedErrors: {
maskError(error, message, isDev) {
if (error?.extensions?.code === 'DOWNSTREAM_SERVICE_ERROR') {
return error
}

return maskError(error, message, isDev)
}
}
})
```

0 comments on commit 03597a5

Please sign in to comment.