• v2
  • Features
  • Error Masking

Error Masking

Yoga uses the Envelop useMaskedErrors for automatically masking unexpected errors and preventing sensitive information from leaking to clients.

Such errors could be caused by failing to establish connections to remote services such as databases or HTTP APIs. Nobody external needs to know that your database server is not reachable. Exposing such information to the outside world can make you vulnerable to targeted attacks.

Getting Started

Lets setup a simple schema that calls a remote service that is unavailable.

import { createServer } from '@graphql-yoga/node'
import { fetch } from '@whatwg-node/fetch'
 
// Provide your schema
const server = createServer({
  schema: {
    typeDefs: /* GraphQL */ `
      type Query {
        greeting: String!
      }
    `,
    resolvers: {
      Query: {
        async greeting() {
          // This service does not exist
          const response = await fetch('http://localhost:9876/greeting')
          const greeting = await response.text()
          return greeting
        },
      },
    },
  },
})
 
// Start the server and explore http://localhost:4000/graphql
server.start()

Executing the following operation results in an execution result that does not include any detail from the error raised by the fetch call.

GraphQL Operation

query {
  greeting
}

Execution Result with Leaking Error Message

{
  "errors": [
    {
      "message": "Unexpected error.",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": ["greetings"]
    }
  ],
  "data": null
}

As you can see Yoga comes with sensible defaults as error masking is enabled without you needing to configure anything.

Receive Original Error in Development

When developing locally seeing the original error within your Chrome Dev Tools might be handy for debugging. You might be tempted to disable the masked errors via the maskedErrors config option, however, we do not recommend that at all!. Having development and production behavior as close as possible is very important for not having any surprises in production.

Instead, we recommend enabling the Yoga development mode.

To do this you need to start Yoga with the NODE_ENV environment variable set to development.

On UNIX and Windows systems the environment variable can be set when starting the server.

# Unix (Linux/MacOS)
NODE_ENV=development node server.js
# Windows
set NODE_ENV=development
node server.js
import { createServer } from '@graphql-yoga/node'
import { fetch } from '@whatwg-node/fetch'
 
// Provide your schema
const server = createServer({
  schema: {
    typeDefs: /* GraphQL */ `
      type Query {
        greeting: String!
      }
    `,
    resolvers: {
      Query: {
        async greeting() {
          // This service does not exist
          const response = await fetch('http://localhost:9876/greeting')
          const greeting = await response.text()
          return greeting
        },
      },
    },
  },
})
 
server.start()

GraphQL Operation

query {
  greeting
}

This will add a more detailed error with a proper stacktrace to the errors extensions.

GraphQL Error Response with Original Error Extensions

{
  "errors": [
    {
      "message": "Unexpected error.",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": ["greeting"],
      "extensions": {
        "originalError": {
          "message": "request to http://localhost:9876/greeting failed, reason: connect ECONNREFUSED 127.0.0.1:9876",
          "stack": "FetchError: request to http://localhost:9876/greeting failed, reason: connect ECONNREFUSED 127.0.0.1:9876\n    at ClientRequest.<anonymous> (C:\\Users\\XXXX\\Projects\\graphql-yoga\\node_modules\\node-fetch\\lib\\index.js:1483:11)\n    at ClientRequest.emit (events.js:376:20)\n    at Socket.socketErrorListener (_http_client.js:475:9)\n    at Socket.emit (events.js:376:20)\n    at emitErrorNT (internal/streams/destroy.js:106:8)\n    at emitErrorCloseNT (internal/streams/destroy.js:74:3)\n    at processTicksAndRejections (internal/process/task_queues.js:82:21)"
        }
      }
    }
  ],
  "data": null
}

Exposing Expected Errors

Sometimes it is feasible to throw errors within your GraphQL resolvers whose message should be sent to clients instead of being masked. This can be achieved by throwing a GraphQLYogaError instead of a "normal" Error.

For example, you might want to throw an error if a resource cannot be found by an ID.

import { createServer, GraphQLYogaError } from '@graphql-yoga/node'
 
const users = [
  { id: '1', login: 'Laurin' },
  { id: '2', login: 'Saihaj' },
  { id: '3', login: 'Dotan' },
]
 
// Provide your schema
const server = createServer({
  schema: {
    typeDefs: /* GraphQL */ `
      type User {
        id: ID!
        login: String!
      }
      type Query {
        user(byId: ID!): User!
      }
    `,
    resolvers: {
      Query: {
        async user(_, args) {
          const user = users.find((user) => user.id === args.byId)
          if (!user) {
            throw new GraphQLYogaError(`User with id '${args.byId}' not found.`)
          }
 
          return user
        },
      },
    },
  },
})
 
// Start the server and explore http://localhost:4000/graphql
server.start()

Query for Non-Existing User

query {
  user(byId: "6") {
    id
  }
}

Execution Result with Error Message

{
  "errors": [
    {
      "message": "User with id '6' not found.",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": ["user"]
    }
  ],
  "data": null
}

Error Codes and Other Extensions

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.

import { createServer, GraphQLYogaError } from '@graphql-yoga/node'
 
const users = [
  { id: '1', login: 'Laurin' },
  { id: '2', login: 'Saihaj' },
  { id: '3', login: 'Dotan' },
]
 
// Provide your schema
const server = createServer({
  schema: {
    typeDefs: /* GraphQL */ `
      type User {
        id: ID!
        login: String!
      }
      type Query {
        user(byId: ID!): User!
      }
    `,
    resolvers: {
      Query: {
        async user(_, args) {
          const user = users.find((user) => user.id === args.byId)
          if (!user) {
            throw new GraphQLYogaError(
              `User with id '${args.byId}' not found.`,
              // error extensions
              { code: 'USER_NOT_FOUND' },
            )
          }
 
          return user
        },
      },
    },
  },
})
 
// Start the server and explore http://localhost:4000/graphql
server.start()

Query for Non-Existing User

query {
  user(byId: "6") {
    id
  }
}

Execution Result with Error Message

{
  "errors": [
    {
      "message": "User with id '6' not found.",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": ["user"],
      "extensions": {
        "code": "USER_NOT_FOUND"
      }
    }
  ],
  "data": null
}

The extensions are not only limited to a code property. Any JSON serializable value can be passed as extensions.

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

Disabling Error Masking

We highly recommend using error masking. However, you can still disable it using the maskedErrors config option.

createServer({ maskedErrors: false })

Executing the following operation will now result in a leaking error message that exposes information about internal API calls.

GraphQL Operation

query {
  greeting
}

Execution Result with Leaking Error Message

{
  "errors": [
    {
      "message": "request to http://localhost:9876/greeting failed, reason: connect ECONNREFUSED 127.0.0.1:9876",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": ["greeting"]
    }
  ],
  "data": null
}
Last updated on August 30, 2022