Repository: mcollina/fastify-gql
Pull request #132 (2020-04-03 09:23:19)

Update GraphQL to version 15.0.0
This PR fixes the tests that are broken in #131.

The tests were broken because of changes in the error messages.

The Typescript check also failed because in the `node_modules/graphql-tools` package has two TS errors related to the schema stitching which is not used by this project at all. This can be avoided if the `IResolvers` type is not imported from the `graphql-tools` lib but included in the `types.d.ts` file.


Patch:
diff --git a/README.md b/README.md
index d5c52a8..601e2f2 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # fastify-gql
 
-[![Greenkeeper badge](https://badges.greenkeeper.io/mcollina/fastify-gql.svg)](https://greenkeeper.io/) [![Build Status](https://travis-ci.com/mcollina/fastify-gql.svg?branch=master)](https://travis-ci.com/mcollina/fastify-gql)
+[![Build Status](https://travis-ci.com/mcollina/fastify-gql.svg?branch=master)](https://travis-ci.com/mcollina/fastify-gql)
 
 Fastify GraphQL adapter.
 
@@ -555,7 +555,7 @@ const resolvers = {
     findUser: (_, { id }) => {
       const user = users[id]
       if (user) return users[id]
-      else throw new ErrorWithProps('Invalid User ID', "USER_ID_INVALID", { id, timestamp: Math.round(new Date().getTime()/1000) })
+      else throw new ErrorWithProps('Invalid User ID', { id, code: "USER_ID_INVALID", timestamp: Math.round(new Date().getTime()/1000) })
     }
   }
 }
diff --git a/examples/gateway.js b/examples/gateway.js
index 2e20441..08578e9 100644
--- a/examples/gateway.js
+++ b/examples/gateway.js
@@ -1,6 +1,7 @@
 'use strict'
 const Fastify = require('fastify')
 const GQL = require('..')
+const { ErrorWithProps } = GQL
 
 async function createService (port, schema, resolvers = {}) {
   const service = Fastify()
@@ -61,6 +62,7 @@ async function start () {
   await createService(4001, `
     extend type Query {
       me: User
+      you: User
       hello: String
     }
 
@@ -82,6 +84,9 @@ async function start () {
       me: (root, args, context, info) => {
         return users.u1
       },
+      you: (root, args, context, info) => {
+        throw new ErrorWithProps('Can\'t fetch other users data', { code: 'NOT_ALLOWED' })
+      },
       hello: () => 'world'
     },
     User: {
diff --git a/index.d.ts b/index.d.ts
index bee519c..e2139e6 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -140,15 +140,11 @@ declare namespace fastifyGQL {
    * Extended errors for adding additional information in error responses
    */
   export class ErrorWithProps extends Error {
-    constructor (message: string, code?: string, additionalProperties?: object)
-    /**
-     * Custom error code of this error
-     */
-    code?: string
+    constructor (message: string, extensions?: object)
     /**
      * Custom additional properties of this error
      */
-    additionalProperties?: object
+    extensions?: object
   } 
 }
 
@@ -260,4 +256,4 @@ type Request = {
   document: DocumentNode;
   variables: Record<string, any>;
   extensions?: Record<string, any>;
-}; 
\ No newline at end of file
+}; 
diff --git a/index.js b/index.js
index 050e067..1469273 100644
--- a/index.js
+++ b/index.js
@@ -28,7 +28,7 @@ const buildFederationSchema = require('./lib/federation')
 const buildGateway = require('./lib/gateway')
 const mq = require('mqemitter')
 const { PubSub } = require('./lib/subscriber')
-const { ErrorWithProps } = require('./lib/errors')
+const { ErrorWithProps, FEDERATED_ERROR } = require('./lib/errors')
 
 const kLoaders = Symbol('fastify-gql.loaders')
 
@@ -360,6 +360,20 @@ const plugin = fp(async function (app, opts) {
 
     if (cached && cached.jit !== null) {
       const res = await cached.jit.query(root, context, variables || {})
+
+      // as `graphql-jit` takes the original response that's dispatched
+      // by the request handler none of the transformations applied do have an effect,
+      // so `FederatedError` needs to be exchanged
+      // with the contents of its `extensions.errors` property and flattened afterwards
+      if (res.errors) {
+        res.errors = res.errors.map((error, idx) => {
+          if (error.message === FEDERATED_ERROR.toString()) {
+            return error.extensions.errors
+          }
+          return error
+        }).reduce((acc, val) => acc.concat(val), [])
+      }
+
       return res
     }
 
@@ -384,11 +398,8 @@ const plugin = fp(async function (app, opts) {
 
     if (execution.errors) {
       execution.errors = execution.errors.map(e => {
-        if (e.originalError && (Object.prototype.hasOwnProperty.call(e.originalError, 'code') || Object.prototype.hasOwnProperty.call(e.originalError, 'additionalProperties'))) {
-          return new GraphQLError(e.originalError.message, e.nodes, e.source, e.positions, e.path, e.originalError, {
-            code: e.originalError.code,
-            ...e.originalError.additionalProperties
-          })
+        if (e.originalError && (Object.prototype.hasOwnProperty.call(e.originalError, 'extensions'))) {
+          return new GraphQLError(e.originalError.message, e.nodes, e.source, e.positions, e.path, e.originalError, e.originalError.extensions)
         }
         return e
       })
diff --git a/lib/errors.js b/lib/errors.js
index 0684d14..fc0fb54 100644
--- a/lib/errors.js
+++ b/lib/errors.js
@@ -1,13 +1,67 @@
 'use strict'
 
+const { GraphQLError } = require('graphql')
+
 class ErrorWithProps extends Error {
-  constructor (message, code, additionalProperties) {
+  constructor (message, extensions) {
     super(message)
-    this.code = code
-    this.additionalProperties = additionalProperties
+    this.extensions = extensions
   }
 }
 
+const FEDERATED_ERROR = Symbol('FEDERATED_ERROR')
+
+// a specialized `Error` which extends the `Error` built-in
+// to satisfy the `graphql` error handler
+class FederatedError extends Error {
+  constructor (errors) {
+    super(FEDERATED_ERROR.toString())
+    this.extensions = { errors }
+  }
+}
+
+// converts an error to a `GraphQLError` compatible
+// allows to copy the `path` & `locations` properties
+// from the already serialized error
+function toGraphQLError (err) {
+  return Object.create(GraphQLError, {
+    name: {
+      value: err.name
+    },
+    message: {
+      value: err.message,
+      enumerable: true,
+      writable: true
+    },
+    locations: {
+      value: err.locations || undefined,
+      enumerable: true
+    },
+    path: {
+      value: err.path || undefined,
+      enumerable: true
+    },
+    nodes: {
+      value: err.nodes || undefined
+    },
+    source: {
+      value: err.source || undefined
+    },
+    positions: {
+      value: err.positions || undefined
+    },
+    originalError: {
+      value: err.originalError || undefined
+    },
+    extensions: {
+      value: err.extensions || undefined
+    }
+  })
+}
+
 module.exports = {
-  ErrorWithProps: ErrorWithProps
+  FEDERATED_ERROR: FEDERATED_ERROR,
+  ErrorWithProps: ErrorWithProps,
+  FederatedError: FederatedError,
+  toGraphQLError: toGraphQLError
 }
diff --git a/lib/gateway/request.js b/lib/gateway/request.js
index 36a6bff..6b15901 100644
--- a/lib/gateway/request.js
+++ b/lib/gateway/request.js
@@ -2,6 +2,7 @@
 const http = require('http')
 const https = require('https')
 const eos = require('end-of-stream')
+const { FederatedError } = require('../errors')
 
 function agentOption (opts) {
   return {
@@ -89,7 +90,9 @@ function sendRequest (request, url) {
             const json = JSON.parse(data.toString())
 
             if (json.errors && json.errors.length) {
-              return reject(json.errors)
+              // return a `FederatedError` instance to keep `graphql` happy
+              // e.g. have something that derives from `Error`
+              return reject(new FederatedError(json.errors))
             }
 
             resolve({
diff --git a/lib/routes.js b/lib/routes.js
index 122abbc..8e13a98 100644
--- a/lib/routes.js
+++ b/lib/routes.js
@@ -5,6 +5,7 @@ const Static = require('fastify-static')
 const { BadRequest } = require('http-errors')
 const { formatError, GraphQLError } = require('graphql')
 const subscription = require('./subscription')
+const { FEDERATED_ERROR, toGraphQLError } = require('./errors')
 
 const responseSchema = {
   '2xx': {
@@ -57,9 +58,15 @@ async function defaultErrorHandler (err, request, reply) {
   }
 
   if (err.errors) {
-    const errors = err.errors.map(error => {
+    const errors = err.errors.map((error, idx) => {
+      // parses, converts & combines errors if they are the result of a federated request
+      if (error.message === FEDERATED_ERROR.toString()) {
+        return error.extensions.errors.map(err => formatError(toGraphQLError(err)))
+      }
       return error instanceof GraphQLError ? formatError(error) : { message: error.message }
-    })
+      // as the result of the outer map could potentially contain arrays with federated errors
+      // the result needs to be flattened
+    }).reduce((acc, val) => acc.concat(val), [])
 
     return { errors, data: err.data || null }
   } else {
diff --git a/package.json b/package.json
index e9798af..b466575 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "fastify-gql",
-  "version": "3.8.0",
+  "version": "3.9.0",
   "description": "Fastify GraphQL adapter",
   "main": "index.js",
   "types": "index.d.ts",
@@ -24,10 +24,10 @@
   },
   "homepage": "https://github.com/mcollina/fastify-gql#readme",
   "devDependencies": {
-    "@types/node": "^13.5.0",
+    "@types/node": "^14.0.1",
     "@typescript-eslint/eslint-plugin": "^2.19.2",
     "@typescript-eslint/parser": "^2.19.2",
-    "autocannon": "^4.5.1",
+    "autocannon": "^5.0.0",
     "fastify": "^2.12.0",
     "graphql-tools": "^4.0.6",
     "pre-commit": "^1.2.2",
@@ -35,21 +35,21 @@
     "snazzy": "^8.0.0",
     "standard": "^14.0.0",
     "tap": "^14.10.6",
-    "typescript": "^3.7.5",
-    "ws": "^7.2.1"
+    "typescript": "^3.7.5"
   },
   "dependencies": {
     "end-of-stream": "^1.4.4",
     "fastify-plugin": "^1.6.0",
     "fastify-static": "^2.6.0",
     "fastify-websocket": "^1.0.0",
-    "graphql": "^14.6.0",
+    "graphql": "^15.0.0",
     "graphql-jit": "^0.4.0",
     "http-errors": "^1.7.3",
     "mqemitter": "^4.0.0",
     "p-map": "^4.0.0",
     "readable-stream": "^3.5.0",
     "single-user-cache": "^0.3.0",
-    "tiny-lru": "^7.0.2"
+    "tiny-lru": "^7.0.2",
+    "ws": "^7.2.1"
   }
 }


Test patch:
diff --git a/tap-snapshots/test-federation.js-TAP.test.js b/tap-snapshots/test-federation.js-TAP.test.js
index f14795b..29f108e 100644
--- a/tap-snapshots/test-federation.js-TAP.test.js
+++ b/tap-snapshots/test-federation.js-TAP.test.js
@@ -20,28 +20,28 @@ directive @customdir on FIELD_DEFINITION
 
 scalar _Any
 
-union _Entity = User | Product
-
 scalar _FieldSet
 
 type _Service {
   sdl: String
 }
 
-type Product {
-  sku: String
-}
-
 type Query {
   me: User
   _entities(representations: [_Any!]!): [_Entity]!
   _service: _Service!
 }
 
+type Product {
+  sku: String
+}
+
 type User {
   id: ID!
   name: String
   username: String
 }
 
+union _Entity = Product | User
+
 `
diff --git a/tap-snapshots/test-reply-decorator.js-TAP.test.js b/tap-snapshots/test-reply-decorator.js-TAP.test.js
index 9197e73..f47f10d 100644
--- a/tap-snapshots/test-reply-decorator.js-TAP.test.js
+++ b/tap-snapshots/test-reply-decorator.js-TAP.test.js
@@ -6,5 +6,5 @@
  */
 'use strict'
 exports['test/reply-decorator.js TAP reply decorator set status code to 400 with bad query > must match snapshot 1'] = `
-{"errors":[{"message":"Syntax Error: Expected Name, found <EOF>","locations":[{"line":1,"column":18}]}]}
+{"errors":[{"message":"Syntax Error: Expected Name, found <EOF>.","locations":[{"line":1,"column":18}]}]}
 `
diff --git a/tap-snapshots/test-routes.js-TAP.test.js b/tap-snapshots/test-routes.js-TAP.test.js
index 744536f..ec7602a 100644
--- a/tap-snapshots/test-routes.js-TAP.test.js
+++ b/tap-snapshots/test-routes.js-TAP.test.js
@@ -53,7 +53,7 @@ exports['test/routes.js TAP POST return 400 on error > must match snapshot 1'] =
 {
   "errors": [
     {
-      "message": "Syntax Error: Expected Name, found <EOF>",
+      "message": "Syntax Error: Expected Name, found <EOF>.",
       "locations": [
         {
           "line": 1,
diff --git a/test/app-decorator.js b/test/app-decorator.js
index 686bd27..37b521d 100644
--- a/test/app-decorator.js
+++ b/test/app-decorator.js
@@ -379,7 +379,12 @@ test('extendSchema and defineResolvers throws without mutation definition', asyn
 
   const mutation = 'mutation { sub(x: 2, y: 2) }'
 
-  t.rejects(app.graphql(mutation), GraphQLError)
+  try {
+    await app.graphql(mutation)
+  } catch (e) {
+    t.is(e instanceof GraphQLError, true)
+    t.end()
+  }
 })
 
 test('basic GQL no cache', async (t) => {
diff --git a/test/errors.js b/test/errors.js
index 9358e40..652ad27 100644
--- a/test/errors.js
+++ b/test/errors.js
@@ -4,23 +4,20 @@ const { test } = require('tap')
 const Fastify = require('fastify')
 const GQL = require('..')
 const { ErrorWithProps } = GQL
+const { FederatedError } = require('../lib/errors')
 
 test('errors - multiple extended errors', async (t) => {
   const schema = `
     type Query {
-      errorOne: String
-      errorTwo: String
+      error: String
       successful: String
     }
   `
 
   const resolvers = {
     Query: {
-      errorOne () {
-        throw new ErrorWithProps('Error One', 'ERROR_ONE', { additional: 'information one', other: 'data one' })
-      },
-      errorTwo () {
-        throw new ErrorWithProps('Error Two', 'ERROR_TWO', { additional: 'information two', other: 'data two' })
+      error () {
+        throw new ErrorWithProps('Error', { code: 'ERROR', additional: 'information', other: 'data' })
       },
       successful () {
         return 'Runs OK'
@@ -39,52 +36,36 @@ test('errors - multiple extended errors', async (t) => {
 
   const res = await app.inject({
     method: 'GET',
-    url: '/graphql?query={errorOne,errorTwo, successful}'
+    url: '/graphql?query={error,successful}'
   })
 
   t.equal(res.statusCode, 200)
   t.deepEqual(JSON.parse(res.payload), {
     data: {
-      errorOne: null,
-      errorTwo: null,
+      error: null,
       successful: 'Runs OK'
     },
     errors: [
       {
-        message: 'Error One',
+        message: 'Error',
         locations: [
           {
             line: 1,
             column: 2
           }
         ],
-        path: ['errorOne'],
+        path: ['error'],
         extensions: {
-          code: 'ERROR_ONE',
-          additional: 'information one',
-          other: 'data one'
-        }
-      },
-      {
-        message: 'Error Two',
-        locations: [
-          {
-            line: 1,
-            column: 11
-          }
-        ],
-        path: ['errorTwo'],
-        extensions: {
-          code: 'ERROR_TWO',
-          additional: 'information two',
-          other: 'data two'
+          code: 'ERROR',
+          additional: 'information',
+          other: 'data'
         }
       }
     ]
   })
 })
 
-test('errors - extended errors with number additionalProperties', async (t) => {
+test('errors - extended errors with number extensions', async (t) => {
   const schema = `
     type Query {
       willThrow: String
@@ -94,7 +75,7 @@ test('errors - extended errors with number additionalProperties', async (t) => {
   const resolvers = {
     Query: {
       willThrow () {
-        throw new ErrorWithProps('Extended Error', 'EXTENDED_ERROR', { floating: 3.14, timestamp: 1324356, reason: 'some reason' })
+        throw new ErrorWithProps('Extended Error', { code: 'EXTENDED_ERROR', floating: 3.14, timestamp: 1324356, reason: 'some reason' })
       }
     }
   }
@@ -155,13 +136,7 @@ test('errors - extended errors optional parameters', async (t) => {
         throw new ErrorWithProps('Extended Error')
       },
       two () {
-        throw new ErrorWithProps('Extended Error', 'ERROR_TWO')
-      },
-      three () {
-        throw new ErrorWithProps('Extended Error', 'ERROR_THREE', { reason: 'some reason' })
-      },
-      four () {
-        throw new ErrorWithProps('Extended Error', undefined, { reason: 'some reason' })
+        throw new ErrorWithProps('Extended Error', { code: 'ERROR_TWO', reason: 'some reason' })
       }
     }
   }
@@ -177,16 +152,14 @@ test('errors - extended errors optional parameters', async (t) => {
 
   const res = await app.inject({
     method: 'GET',
-    url: '/graphql?query={one,two,three,four}'
+    url: '/graphql?query={one,two}'
   })
 
   t.equal(res.statusCode, 200)
   t.deepEqual(JSON.parse(res.payload), {
     data: {
       one: null,
-      two: null,
-      three: null,
-      four: null
+      two: null
     },
     errors: [
       {
@@ -197,8 +170,7 @@ test('errors - extended errors optional parameters', async (t) => {
             column: 2
           }
         ],
-        path: ['one'],
-        extensions: {}
+        path: ['one']
       },
       {
         message: 'Extended Error',
@@ -210,36 +182,203 @@ test('errors - extended errors optional parameters', async (t) => {
         ],
         path: ['two'],
         extensions: {
-          code: 'ERROR_TWO'
+          code: 'ERROR_TWO',
+          reason: 'some reason'
         }
+      }
+    ]
+  })
+})
+
+test('errors - errors with jit enabled', async (t) => {
+  const schema = `
+    type Query {
+      error: String
+      successful: String
+    }
+  `
+
+  const resolvers = {
+    Query: {
+      error () {
+        throw new ErrorWithProps('Error', { code: 'ERROR', additional: 'information', other: 'data' })
       },
+      successful () {
+        return 'Runs OK'
+      }
+    }
+  }
+
+  const app = Fastify()
+
+  app.register(GQL, {
+    schema,
+    resolvers,
+    jit: 1
+  })
+
+  await app.ready()
+
+  await app.inject({
+    method: 'GET',
+    url: '/graphql?query={error,successful}'
+  })
+
+  const res = await app.inject({
+    method: 'GET',
+    url: '/graphql?query={error,successful}'
+  })
+
+  t.equal(res.statusCode, 200)
+  t.deepEqual(JSON.parse(res.payload), {
+    data: {
+      error: null,
+      successful: 'Runs OK'
+    },
+    errors: [
       {
-        message: 'Extended Error',
+        message: 'Error',
         locations: [
           {
             line: 1,
-            column: 10
+            column: 2
           }
         ],
-        path: ['three'],
+        path: ['error'],
         extensions: {
-          code: 'ERROR_THREE',
-          reason: 'some reason'
+          code: 'ERROR',
+          additional: 'information',
+          other: 'data'
         }
+      }
+    ]
+  })
+})
+
+test('errors - federated errors with jit enabled', async (t) => {
+  const schema = `
+    type Query {
+      error: String
+      successful: String
+    }
+  `
+
+  const resolvers = {
+    Query: {
+      error () {
+        throw new FederatedError([{
+          message: 'Invalid operation',
+          locations: [{ column: 3, line: 2 }],
+          path: ['error'],
+          extensions: {
+            code: 'ERROR',
+            additional: 'information',
+            other: 'data'
+          }
+        }])
       },
+      successful () {
+        return 'Runs OK'
+      }
+    }
+  }
+
+  const app = Fastify()
+
+  app.register(GQL, {
+    schema,
+    resolvers,
+    jit: 1
+  })
+
+  await app.ready()
+
+  const res = await app.inject({
+    method: 'GET',
+    url: '/graphql?query={error,successful}'
+  })
+
+  const jitres = await app.inject({
+    method: 'GET',
+    url: '/graphql?query={error,successful}'
+  })
+
+  const expectedResult = {
+    data: {
+      error: null,
+      successful: 'Runs OK'
+    },
+    errors: [
       {
-        message: 'Extended Error',
-        locations: [
-          {
-            line: 1,
-            column: 16
-          }
-        ],
-        path: ['four'],
+        message: 'Invalid operation',
+        locations: [{ column: 3, line: 2 }],
+        path: ['error'],
         extensions: {
-          reason: 'some reason'
+          code: 'ERROR',
+          additional: 'information',
+          other: 'data'
         }
       }
     ]
+  }
+
+  t.equal(res.statusCode, 200)
+  t.deepEqual(JSON.parse(res.payload), expectedResult)
+
+  t.equal(jitres.statusCode, 200)
+  t.deepEqual(JSON.parse(jitres.payload), expectedResult)
+})
+
+test('errors - federated errors without locations, path and extensions', async (t) => {
+  const schema = `
+    type Query {
+      error: String
+      successful: String
+    }
+  `
+
+  const resolvers = {
+    Query: {
+      error () {
+        throw new FederatedError([{ message: 'Invalid operation' }])
+      },
+      successful () {
+        return 'Runs OK'
+      }
+    }
+  }
+
+  const app = Fastify()
+
+  app.register(GQL, {
+    schema,
+    resolvers,
+    jit: 1
+  })
+
+  await app.ready()
+
+  const res = await app.inject({
+    method: 'GET',
+    url: '/graphql?query={error,successful}'
+  })
+
+  const jitres = await app.inject({
+    method: 'GET',
+    url: '/graphql?query={error,successful}'
   })
+
+  const expectedResult = {
+    data: {
+      error: null,
+      successful: 'Runs OK'
+    },
+    errors: [{ message: 'Invalid operation' }]
+  }
+
+  t.equal(res.statusCode, 200)
+  t.deepEqual(JSON.parse(res.payload), expectedResult)
+
+  t.equal(jitres.statusCode, 200)
+  t.deepEqual(JSON.parse(jitres.payload), expectedResult)
 })
diff --git a/test/gateway/extends-directive.js b/test/gateway/extends-directive.js
index f162789..0ad968a 100644
--- a/test/gateway/extends-directive.js
+++ b/test/gateway/extends-directive.js
@@ -76,7 +76,7 @@ test('gateway handles @extends directive correctly', async (t) => {
       author: User @requires(fields: "title")
     }
 
-    type Query @extends {
+    extend type Query {
       topPosts(count: Int): [Post]
     }
 
diff --git a/test/routes.js b/test/routes.js
index 1a696cd..5664a68 100644
--- a/test/routes.js
+++ b/test/routes.js
@@ -6,6 +6,7 @@ const querystring = require('querystring')
 const WebSocket = require('ws')
 const { GraphQLError } = require('graphql')
 const GQL = require('..')
+const { FederatedError } = require('../lib/errors')
 
 test('POST route', async (t) => {
   const app = Fastify()
@@ -1104,7 +1105,7 @@ test('Error handler set to true should not change default behavior', async (t) =
 
   const expectedResult = {
     errors: [{
-      message: 'Expected type Int, found "2".',
+      message: 'Int cannot represent non-integer value: "2"',
       locations: [{
         line: 1,
         column: 8
@@ -1179,6 +1180,53 @@ test('route validation is catched and parsed to graphql error', async (t) => {
   t.strictDeepEqual(JSON.parse(res.body), expectedResult)
 })
 
+test('Error handler flattens federated errors', async (t) => {
+  const app = Fastify()
+  const schema = `
+    type Query {
+      add(x: Int, y: Int): Int
+    }
+  `
+
+  const resolvers = {
+    add: async ({ x, y }) => {
+      throw new FederatedError([{
+        message: 'Service error',
+        path: ['add'],
+        locations: [{ column: 3, line: 2 }],
+        extensions: { code: 'NOT_IMPLEMENTED' }
+      }])
+    }
+  }
+
+  app.register(GQL, {
+    schema,
+    resolvers,
+    errorHandler: true
+  })
+
+  // Invalid query
+  const res = await app.inject({
+    method: 'GET',
+    url: '/graphql?query={add(x:2,y:2)}'
+  })
+
+  const expectedResult = {
+    errors: [{
+      message: 'Service error',
+      path: ['add'],
+      locations: [{ column: 3, line: 2 }],
+      extensions: { code: 'NOT_IMPLEMENTED' }
+    }],
+    data: {
+      add: null
+    }
+  }
+
+  t.strictEqual(res.statusCode, 200)
+  t.strictDeepEqual(JSON.parse(res.body), expectedResult)
+})
+
 test('routes with custom context', async (t) => {
   const app = Fastify()
 
diff --git a/test/types/index.ts b/test/types/index.ts
index 28e779c..60953e9 100644
--- a/test/types/index.ts
+++ b/test/types/index.ts
@@ -89,7 +89,7 @@ app.register(async function (app) {
   `)
   app.graphql.defineResolvers({
     Query: {
-      willThrow: async () => { throw new ErrorWithProps('Extended Error', 'EXTENDED_ERROR', { reason: 'some reason', other: 32 }) }
+      willThrow: async () => { throw new ErrorWithProps('Extended Error', { code: 'EXTENDED_ERROR', reason: 'some reason', other: 32 }) }
     }
   })
 })