Repository: mercurius-js/mercurius
Pull request #1105 (2024-08-09 09:43:42)

Error through subscription wrapped in an array
The graphql-ws protocol specifies that the payload of an `'error'` message should be an array of GraphQL errors. ([ref](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md#error))

To accommodate this, I created the sendError function to manage the transformation and updated all related tests accordingly.


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 68af50d..eec220e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -4,8 +4,9 @@ jobs:
   test:
     runs-on: ${{ matrix.os }}
     strategy:
+      fail-fast: false
       matrix:
-        node-version: [16.x, 18.x, 20.x]
+        node-version: [20.x, 22.x]
         os: [ubuntu-latest, windows-latest, macOS-latest]
     steps:
       - uses: actions/checkout@v4
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..43c97e7
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,1 @@
+package-lock=false
diff --git a/README.md b/README.md
index 6974f48..e05b09b 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,10 @@
 
 # mercurius
 
-![CI workflow](https://github.com/mercurius-js/mercurius/workflows/CI%20workflow/badge.svg)
+[![CI workflow](https://github.com/mercurius-js/mercurius/actions/workflows/ci.yml/badge.svg)](https://github.com/mercurius-js/mercurius/actions/workflows/ci.yml)
+[![NPM version](https://img.shields.io/npm/v/mercurius.svg?style=flat)](https://www.npmjs.com/package/mercurius)
+[![NPM downloads](https://img.shields.io/npm/dm/mercurius.svg?style=flat)](https://www.npmjs.com/package/mercurius)
+[![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard)
 
 Mercurius is a [**GraphQL**](https://graphql.org/) adapter for [**Fastify**](https://www.fastify.io)
 
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..6b8c5cf
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,5 @@
+'use strict'
+
+module.exports = require('neostandard')({
+  ts: true,
+})
diff --git a/index.d.ts b/index.d.ts
index 55ded72..1339620 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -4,7 +4,7 @@ import {
   FastifyRequest,
   FastifyInstance,
   RouteOptions
-} from "fastify";
+} from 'fastify'
 import {
   DocumentNode,
   ExecutionResult,
@@ -15,10 +15,10 @@ import {
   ValidationRule,
   FormattedExecutionResult,
   ParseOptions,
-} from "graphql";
-import type { WebSocket } from "ws";
-import { IncomingMessage, OutgoingHttpHeaders } from "http";
-import { Readable } from "stream";
+} from 'graphql'
+import type { WebSocket } from 'ws'
+import { IncomingMessage, OutgoingHttpHeaders } from 'http'
+import { Readable } from 'stream'
 
 type Mercurius = typeof mercurius
 
@@ -27,7 +27,7 @@ declare namespace mercurius {
     subscribe<TResult = any>(topics: string | string[]): Promise<Readable & AsyncIterableIterator<TResult>>;
     publish<TResult = any>(event: { topic: string; payload: TResult }, callback?: () => void): void;
   }
-  
+
   export interface MercuriusContext {
     app: FastifyInstance;
     reply: FastifyReply;
@@ -39,11 +39,11 @@ declare namespace mercurius {
      */
     pubsub: PubSub;
   }
-  
+
   export interface MercuriusError<TError extends Error = Error> extends FastifyError {
     errors?: TError[]
   }
-  
+
   export interface Loader<
     TObj extends Record<string, any> = any,
     TParams extends Record<string, any> = any,
@@ -59,24 +59,24 @@ declare namespace mercurius {
       }
     ): any;
   }
-  
+
   export interface MercuriusLoaders<TContext extends Record<string, any> = MercuriusContext> {
     [root: string]: {
       [field: string]:
         | Loader<any, any, TContext>
         | {
-            loader: Loader<any, any, TContext>;
-            opts?: {
-              cache?: boolean;
-            };
+          loader: Loader<any, any, TContext>;
+          opts?: {
+            cache?: boolean;
           };
+        };
     };
   }
-  
+
   // ------------------------
   // Request Lifecycle hooks
   // ------------------------
-  
+
   /**
    * `preParsing` is the first hook to be executed in the GraphQL request lifecycle. The next hook will be `preValidation`.
    */
@@ -87,7 +87,7 @@ declare namespace mercurius {
       context: TContext,
     ): Promise<void> | void;
   }
-  
+
   /**
    * `preValidation` is the second hook to be executed in the GraphQL request lifecycle. The previous hook was `preParsing`, the next hook will be `preExecution`.
    */
@@ -98,7 +98,7 @@ declare namespace mercurius {
       context: TContext,
     ): Promise<void> | void;
   }
-  
+
   /**
    * `preExecution` is the third hook to be executed in the GraphQL request lifecycle. The previous hook was `preValidation`.
    * Notice: in the `preExecution` hook, you can modify the following items by returning them in the hook definition:
@@ -115,7 +115,7 @@ declare namespace mercurius {
       variables: Record<string, any>,
     ): Promise<PreExecutionHookResponse<TError> | void> | PreExecutionHookResponse<TError> | void;
   }
-  
+
   /**
    * `onResolution` is the fifth and final hook to be executed in the GraphQL request lifecycle. The previous hook was `preExecution`.
    */
@@ -125,11 +125,11 @@ declare namespace mercurius {
       context: TContext,
     ): Promise<void> | void;
   }
-  
+
   // -----------------------------
   // Subscription Lifecycle hooks
   // -----------------------------
-  
+
   /**
    * `preSubscriptionParsing` is the first hook to be executed in the GraphQL subscription lifecycle. The next hook will be `preSubscriptionExecution`.
    * This hook will only be triggered when subscriptions are enabled.
@@ -141,7 +141,7 @@ declare namespace mercurius {
       context: TContext,
     ): Promise<void> | void;
   }
-  
+
   /**
    * `preSubscriptionExecution` is the second hook to be executed in the GraphQL subscription lifecycle. The previous hook was `preSubscriptionParsing`.
    * This hook will only be triggered when subscriptions are enabled.
@@ -153,7 +153,7 @@ declare namespace mercurius {
       context: TContext,
     ): Promise<void> | void;
   }
-  
+
   /**
    * `onSubscriptionResolution` is the fourth hook to be executed in the GraphQL subscription lifecycle. The next hook will be `onSubscriptionEnd`.
    * This hook will only be triggered when subscriptions are enabled.
@@ -164,7 +164,7 @@ declare namespace mercurius {
       context: TContext,
     ): Promise<void> | void;
   }
-  
+
   /**
    * `onSubscriptionEnd` is the fifth and final hook to be executed in the GraphQL subscription lifecycle. The previous hook was `onSubscriptionResolution`.
    * This hook will only be triggered when subscriptions are enabled.
@@ -175,28 +175,28 @@ declare namespace mercurius {
       id: string | number,
     ): Promise<void> | void;
   }
-  
+
   // ----------------------------
   // Application Lifecycle hooks
   // ----------------------------
-  
+
   export interface onExtendSchemaHandler<TContext = MercuriusContext> {
     (
       schema: GraphQLSchema,
       context: TContext,
     ): Promise<void> | void;
   }
-  
+
   interface ServiceConfig {
     setSchema: (schema: string) => ServiceConfig;
   }
-  
+
   export interface MercuriusPlugin {
     <
       TData extends Record<string, any> = Record<string, any>,
       TVariables extends Record<string, any> = Record<string, any>
     >(
-      source: string,
+      source: string | DocumentNode,
       context?: Record<string, any>,
       variables?: TVariables,
       operationName?: string
@@ -237,21 +237,21 @@ declare namespace mercurius {
      * Managed GraphQL schema object for doing custom execution with. Will reflect changes made via `extendSchema`, `defineResolvers`, etc.
      */
     schema: GraphQLSchema;
-  
+
     // addHook: overloads
-  
+
     // Request lifecycle addHooks
-  
+
     /**
      * `preParsing` is the first hook to be executed in the GraphQL request lifecycle. The next hook will be `preValidation`.
      */
     addHook<TContext = MercuriusContext>(name: 'preParsing', hook: preParsingHookHandler<TContext>): void;
-  
+
     /**
      * `preValidation` is the second hook to be executed in the GraphQL request lifecycle. The previous hook was `preParsing`, the next hook will be `preExecution`.
      */
     addHook<TContext = MercuriusContext>(name: 'preValidation', hook: preValidationHookHandler<TContext>): void;
-  
+
     /**
      * `preExecution` is the third hook to be executed in the GraphQL request lifecycle. The previous hook was `preValidation`.
      * Notice: in the `preExecution` hook, you can modify the following items by returning them in the hook definition:
@@ -259,49 +259,49 @@ declare namespace mercurius {
      *  - `errors`
      */
     addHook<TContext = MercuriusContext, TError extends Error = Error>(name: 'preExecution', hook: preExecutionHookHandler<TContext, TError>): void;
-  
+
     /**
      * `onResolution` is the fifth and final hook to be executed in the GraphQL request lifecycle. The previous hook was `preExecution`.
      */
     addHook<TData extends Record<string, any> = Record<string, any>, TContext = MercuriusContext>(name: 'onResolution', hook: onResolutionHookHandler<TData, TContext>): void;
-  
+
     // Subscription lifecycle addHooks
-  
+
     /**
      * `preSubscriptionParsing` is the first hook to be executed in the GraphQL subscription lifecycle. The next hook will be `preSubscriptionExecution`.
      * This hook will only be triggered when subscriptions are enabled.
      */
     addHook<TContext = MercuriusContext>(name: 'preSubscriptionParsing', hook: preSubscriptionParsingHookHandler<TContext>): void;
-  
+
     /**
      * `preSubscriptionExecution` is the second hook to be executed in the GraphQL subscription lifecycle. The previous hook was `preSubscriptionParsing`.
      * This hook will only be triggered when subscriptions are enabled.
      */
     addHook<TContext = MercuriusContext>(name: 'preSubscriptionExecution', hook: preSubscriptionExecutionHookHandler<TContext>): void;
-  
+
     /**
      * `onSubscriptionResolution` is the fourth and final hook to be executed in the GraphQL subscription lifecycle.
      * This hook will only be triggered when subscriptions are enabled.
      */
     addHook<TData extends Record<string, any> = Record<string, any>, TContext = MercuriusContext>(name: 'onSubscriptionResolution', hook: onSubscriptionResolutionHookHandler<TData, TContext>): void;
-  
+
     /**
      * `onSubscriptionEnd` is the fifth and final hook to be executed in the GraphQL subscription lifecycle. The previous hook was `onSubscriptionResolution`.
      * This hook will only be triggered when subscriptions are enabled.
      */
     addHook<TContext = MercuriusContext>(name: 'onSubscriptionEnd', hook: onSubscriptionEndHookHandler<TContext>): void;
-  
+
     // Application lifecycle addHooks
     addHook<TContext = MercuriusContext>(name: 'onExtendSchema', hook: onExtendSchemaHandler<TContext>): void;
   }
-  
+
   interface QueryRequest {
     operationName?: string;
     query: string;
     variables?: object;
     extensions?: object;
   }
-  
+
   interface WsConnectionParams {
     connectionInitPayload?:
       | (() => Record<string, any> | Promise<Record<string, any>>)
@@ -311,9 +311,9 @@ declare namespace mercurius {
     connectionCallback?: () => void;
     failedConnectionCallback?: (err: { message: string }) => void | Promise<void>;
     failedReconnectCallback?: () => void;
-    rewriteConnectionInitPayload?:  <TContext extends MercuriusContext = MercuriusContext>(payload: Record<string, any> | undefined, context: TContext) => Record<string, any>;
+    rewriteConnectionInitPayload?: <TContext extends MercuriusContext = MercuriusContext>(payload: Record<string, any> | undefined, context: TContext) => Record<string, any>;
   }
-  
+
   export interface MercuriusSchemaOptions {
     /**
      * The GraphQL schema. String schema will be parsed
@@ -332,7 +332,7 @@ declare namespace mercurius {
      */
     schemaTransforms?: ((originalSchema: GraphQLSchema) => GraphQLSchema) | Array<(originalSchema: GraphQLSchema) => GraphQLSchema>;
   }
-  
+
   export interface MercuriusGraphiQLOptions {
     /**
      * Expose the graphiql app, default `true`
@@ -360,16 +360,16 @@ declare namespace mercurius {
       fetcherWrapper?: string;
     }>
   }
-  
+
   export interface GrapQLValidateOptions {
     maxErrors?: number;
   }
-  
+
   export interface MercuriusGraphQLOptions {
     parseOptions?: ParseOptions,
     validateOptions?: GrapQLValidateOptions
   }
-  
+
   export interface MercuriusCommonOptions {
     /**
      * Serve GraphiQL on /graphiql if true or 'graphiql' and if routes is true
@@ -446,33 +446,33 @@ declare namespace mercurius {
     subscription?:
       | boolean
       | {
-          emitter?: object;
-          pubsub?: any; // FIXME: Technically this should be the PubSub type. But PubSub is now typed as SubscriptionContext.
-          verifyClient?: (
-            info: { origin: string; secure: boolean; req: IncomingMessage },
-            next: (
-              result: boolean,
-              code?: number,
-              message?: string,
-              headers?: OutgoingHttpHeaders
-            ) => void
-          ) => void;
-          context?: (
-            socket: WebSocket,
-            request: FastifyRequest
-          ) => Record<string, any> | Promise<Record<string, any>>;
-          onConnect?: (data: {
-            type: 'connection_init';
-            payload: any;
-          }) => any;
-          onDisconnect?: (context: MercuriusContext) => void | Promise<void>;
-          keepAlive?: number,
-          fullWsTransport?: boolean,
-        };
+        emitter?: object;
+        pubsub?: any; // FIXME: Technically this should be the PubSub type. But PubSub is now typed as SubscriptionContext.
+        verifyClient?: (
+          info: { origin: string; secure: boolean; req: IncomingMessage },
+          next: (
+            result: boolean,
+            code?: number,
+            message?: string,
+            headers?: OutgoingHttpHeaders
+          ) => void
+        ) => void;
+        context?: (
+          socket: WebSocket,
+          request: FastifyRequest
+        ) => Record<string, any> | Promise<Record<string, any>>;
+        onConnect?: (data: {
+          type: 'connection_init';
+          payload: any;
+        }) => any;
+        onDisconnect?: (context: MercuriusContext) => void | Promise<void>;
+        keepAlive?: number,
+        fullWsTransport?: boolean,
+      };
     /**
      * Persisted queries, overrides persistedQueryProvider.
      */
-    persistedQueries?: Record<string,string>;
+    persistedQueries?: Record<string, string>;
     /**
      * Only allow persisted queries. Required persistedQueries, overrides persistedQueryProvider.
      */
@@ -481,7 +481,7 @@ declare namespace mercurius {
      * Settings for enabling persisted queries.
      */
     persistedQueryProvider?: PersistedQueryProvider;
-  
+
     /**
      * Enable support for batched queries (POST requests only).
      * Batched query support allows clients to send an array of queries and
@@ -490,18 +490,18 @@ declare namespace mercurius {
     allowBatchedQueries?: boolean;
 
     /**
-     * Customize the graphql routers initialized by mercurius with 
-     * more fastify route options. 
-     * 
+     * Customize the graphql routers initialized by mercurius with
+     * more fastify route options.
+     *
      * Usecases:
-     * - Add more validation 
-     * - change the schema structure 
+     * - Add more validation
+     * - change the schema structure
      * - Hook on the request's life cycle
      * - Increase body size limit for larger queries
     */
-    additionalRouteOptions?:Omit<RouteOptions,"handler"|"wsHandler"|"method"|"url">
+    additionalRouteOptions?: Omit<RouteOptions, 'handler' | 'wsHandler' | 'method' | 'url'>
   }
-  
+
   export type MercuriusOptions = MercuriusCommonOptions & (MercuriusSchemaOptions)
 
   export interface IResolvers<TSource = any, TContext = MercuriusContext> {
@@ -513,7 +513,7 @@ declare namespace mercurius {
       | IEnumResolver
       | undefined;
   }
-  
+
   export type IResolverObject<TSource = any, TContext = MercuriusContext, TArgs = any> = {
     [key: string]:
       | IFieldResolver<TSource, TContext, TArgs>
@@ -521,17 +521,17 @@ declare namespace mercurius {
       | IResolverObject<TSource, TContext>
       | undefined;
   }
-  
+
   export interface IResolverOptions<TSource = any, TContext = MercuriusContext, TArgs = any> {
     fragment?: string;
     resolve?: IFieldResolver<TSource, TContext, TArgs>;
     subscribe?: IFieldResolver<TSource, TContext, TArgs>;
   }
-  
+
   type IEnumResolver = {
     [key: string]: string | number;
-  };
-  
+  }
+
   export interface IFieldResolver<TSource, TContext = MercuriusContext, TArgs = Record<string, any>> {
     (
       source: TSource,
@@ -542,10 +542,10 @@ declare namespace mercurius {
       }
     ): any;
   }
-  
+
   type MergeInfo = {
     delegate: (
-      type: "query" | "mutation" | "subscription",
+      type: 'query' | 'mutation' | 'subscription',
       fieldName: string,
       args: {
         [key: string]: any;
@@ -561,14 +561,14 @@ declare namespace mercurius {
       field: string;
       fragment: string;
     }>;
-  };
-  
+  }
+
   type Transform = {
     transformSchema?: (schema: GraphQLSchema) => GraphQLSchema;
     transformRequest?: (originalRequest: Request) => Request;
     transformResult?: (result: Result) => Result;
-  };
-  
+  }
+
   interface IDelegateToSchemaOptions<
     TContext = {
       [key: string]: any;
@@ -585,31 +585,31 @@ declare namespace mercurius {
     transforms?: Array<Transform>;
     skipValidation?: boolean;
   }
-  
-  type Operation = "query" | "mutation" | "subscription";
-  
+
+  type Operation = 'query' | 'mutation' | 'subscription'
+
   type Result = ExecutionResult & {
     extensions?: Record<string, any>;
-  };
-  
+  }
+
   interface IGraphQLToolsResolveInfo extends GraphQLResolveInfo {
     mergeInfo?: MergeInfo;
   }
-  
+
   type Request = {
     document: DocumentNode;
     variables: Record<string, any>;
     extensions?: Record<string, any>;
-  };
-  
+  }
+
   type ValidationRules =
     | ValidationRule[]
     | ((params: {
-        source: string;
-        variables?: Record<string, any>;
-        operationName?: string;
-      }) => ValidationRule[]);
-  
+      source: string;
+      variables?: Record<string, any>;
+      operationName?: string;
+    }) => ValidationRule[])
+
   export interface PreExecutionHookResponse<TError extends Error> {
     schema?: GraphQLSchema
     document?: DocumentNode
@@ -657,12 +657,12 @@ declare namespace mercurius {
    * Extended errors for adding additional information in error responses
    */
   export class ErrorWithProps extends Error {
-    constructor(message: string, extensions?: object, statusCode?: number);
+    constructor (message: string, extensions?: object, statusCode?: number)
     /**
      * Custom additional properties of this error
      */
-    extensions?: object;
-    statusCode?: number;
+    extensions?: object
+    statusCode?: number
   }
 
   /**
@@ -672,7 +672,7 @@ declare namespace mercurius {
     prepared: (persistedQueries: object) => PersistedQueryProvider;
     preparedOnly: (persistedQueries: object) => PersistedQueryProvider;
     automatic: (maxSize?: number) => PersistedQueryProvider;
-  };
+  }
 
   /**
    * Default error formatter.
@@ -680,7 +680,7 @@ declare namespace mercurius {
   export const defaultErrorFormatter: (
     execution: ExecutionResult & Required<Pick<ExecutionResult, 'errors'>>,
     context: MercuriusContext
-  ) => { statusCode: number; response: FormattedExecutionResult };
+  ) => { statusCode: number; response: FormattedExecutionResult }
 
   /**
    * Subscriptions with filter functionality
@@ -718,7 +718,7 @@ declare namespace mercurius {
   export { mercurius as default }
 }
 
-declare module "fastify" {
+declare module 'fastify' {
   interface FastifyInstance {
     /**
      * GraphQL plugin
@@ -737,7 +737,7 @@ declare module "fastify" {
       TData extends Record<string, any> = Record<string, any>,
       TVariables extends Record<string, any> = Record<string, any>
     >(
-      source: string,
+      source: string | DocumentNode,
       context?: Record<string, any>,
       variables?: TVariables,
       operationName?: string
@@ -752,12 +752,11 @@ declare module "fastify" {
  * @param opts Mercurius options
  */
 declare function mercurius
-  (
-    instance: FastifyInstance,
-    opts: mercurius.MercuriusOptions
-  ): void;
-
+(
+  instance: FastifyInstance,
+  opts: mercurius.MercuriusOptions
+): void
 
 // CJS export
 // const mercurius = require('mercurius')
-export = mercurius;
+export = mercurius
diff --git a/index.js b/index.js
index 4523237..f692f32 100644
--- a/index.js
+++ b/index.js
@@ -1,7 +1,6 @@
 'use strict'
 
 const fp = require('fastify-plugin')
-const LRU = require('tiny-lru').lru
 const routes = require('./lib/routes')
 const { compileQuery, isCompiledQuery } = require('graphql-jit')
 const { Factory } = require('single-user-cache')
@@ -46,7 +45,7 @@ const {
   onExtendSchemaHandler
 } = require('./lib/handlers')
 
-function buildCache (opts) {
+async function buildCache (opts) {
   if (Object.prototype.hasOwnProperty.call(opts, 'cache')) {
     const isBoolean = typeof opts.cache === 'boolean'
     const isNumber = typeof opts.cache === 'number'
@@ -55,20 +54,22 @@ function buildCache (opts) {
       // no cache
       return null
     } else if (isNumber) {
+      const QuickLRU = (await import('quick-lru')).default
       // cache size as specified
-      return LRU(opts.cache)
+      return new QuickLRU({ maxSize: opts.cache })
     } else if (!isBoolean && !isNumber) {
       throw new MER_ERR_INVALID_OPTS('Cache type is not supported')
     }
   }
 
+  const QuickLRU = (await import('quick-lru')).default
   // default cache, 1024 entries
-  return LRU(1024)
+  return new QuickLRU({ maxSize: 1024 })
 }
 
 const mercurius = fp(async function (app, opts) {
-  const lru = buildCache(opts)
-  const lruErrors = buildCache(opts)
+  const lru = await buildCache(opts)
+  const lruErrors = await buildCache(opts)
 
   if (lru && opts.validationRules && typeof opts.validationRules === 'function') {
     throw new MER_ERR_INVALID_OPTS('Using a function for the validationRules is incompatible with query caching')
@@ -413,7 +414,7 @@ const mercurius = fp(async function (app, opts) {
     const reply = context.reply
 
     // Trigger preParsing hook
-    if (context.preParsing !== null) {
+    if (context.preParsing !== null && typeof source === 'string') {
       await preParsingHandler({ schema: fastifyGraphQl.schema, source, context })
     }
 
@@ -433,7 +434,9 @@ const mercurius = fp(async function (app, opts) {
       }
 
       try {
-        document = parse(source, gqlParseOpts)
+        document = typeof source === 'string'
+          ? parse(source, gqlParseOpts)
+          : structuredClone(source)
       } catch (syntaxError) {
         try {
           // Do not try to JSON.parse maxToken exceeded validation errors
@@ -582,7 +585,7 @@ const mercurius = fp(async function (app, opts) {
   }
 }, {
   name: 'mercurius',
-  fastify: '4.x'
+  fastify: '5.x'
 })
 
 mercurius.ErrorWithProps = ErrorWithProps
diff --git a/lib/subscription-connection.js b/lib/subscription-connection.js
index 65d5e1d..4cb4b01 100644
--- a/lib/subscription-connection.js
+++ b/lib/subscription-connection.js
@@ -8,6 +8,7 @@ const { MER_ERR_GQL_SUBSCRIPTION_FORBIDDEN, MER_ERR_GQL_SUBSCRIPTION_UNKNOWN_EXT
 const { preSubscriptionParsingHandler, onSubscriptionResolutionHandler, preSubscriptionExecutionHandler, onSubscriptionEndHandler } = require('./handlers')
 const { kSubscriptionFactory, kLoaders } = require('./symbols')
 const { getProtocolByName } = require('./subscription-protocol')
+const { toGraphQLError } = require('./errors')
 
 module.exports = class SubscriptionConnection {
   constructor (socket, {
@@ -63,7 +64,7 @@ module.exports = class SubscriptionConnection {
     try {
       data = sJSON.parse(isBinary ? message : message.toString())
     } catch (e) {
-      this.sendMessage(this.protocolMessageTypes.GQL_ERROR, null, 'Message must be a JSON string')
+      this.sendError(new Error('Message must be a JSON string'), null)
       return
     }
 
@@ -78,11 +79,7 @@ module.exports = class SubscriptionConnection {
       case this.protocolMessageTypes.GQL_START: {
         if (this.isReady) {
           this.handleGQLStart(data).catch((e) => {
-            this.sendMessage(
-              this.protocolMessageTypes.GQL_ERROR,
-              id,
-              e.message
-            )
+            this.sendError(e, id)
           })
         } else {
           this.sendMessage(
@@ -116,11 +113,7 @@ module.exports = class SubscriptionConnection {
           break
         }
 
-        this.sendMessage(
-          this.protocolMessageTypes.GQL_ERROR,
-          id,
-          'Invalid payload type'
-        )
+        this.sendError(new Error('Invalid payload type'), id)
     }
   }
 
@@ -389,6 +382,12 @@ module.exports = class SubscriptionConnection {
     }
   }
 
+  sendError (err, id) {
+    const convertedError = toGraphQLError(err)
+    // Graphql over websocket spec requires that errors are wrapped in an array
+    this.sendMessage(this.protocolMessageTypes.GQL_ERROR, id, [convertedError])
+  }
+
   async handleConnectionInitExtension (extension) {
     if (typeof this.onConnect === 'function') {
       let authorize = false
diff --git a/package.json b/package.json
index 2c2007e..4dacdcf 100644
--- a/package.json
+++ b/package.json
@@ -8,10 +8,8 @@
     "docs": "docsify serve",
     "unit": "tap test/*.js test/internals/*.js",
     "cov": "tap  --coverage-report=html -J test/*.js test/internals/*.js",
-    "lint": "npm run lint:standard && npm run lint:typescript",
-    "lint:fix": "standard --fix",
-    "lint:standard": "standard | snazzy",
-    "lint:typescript": "standard --parser @typescript-eslint/parser --plugin @typescript-eslint/eslint-plugin test/types/*.ts",
+    "lint": "eslint",
+    "lint:fix": "eslint --fix",
     "typescript": "tsd",
     "test": "npm run lint && npm run unit && npm run typescript",
     "test2": "npm run unit"
@@ -35,36 +33,36 @@
     "@graphql-tools/utils": "^10.0.0",
     "@sinonjs/fake-timers": "^11.0.0",
     "@types/isomorphic-form-data": "^2.0.0",
-    "@types/node": "^20.1.4",
+    "@types/node": "^22.0.0",
     "@types/ws": "^8.2.0",
-    "@typescript-eslint/eslint-plugin": "^5.21.0",
-    "@typescript-eslint/parser": "^5.21.0",
     "autocannon": "^7.3.0",
     "concurrently": "^8.0.1",
     "docsify-cli": "^4.4.3",
-    "fastify": "^4.17.0",
+    "eslint": "^9.9.1",
+    "fastify": "^5.0.0-alpha.4",
     "graphql": "^16.0.0",
+    "graphql-tag": "^2.12.6",
     "graphql-ws": "^5.11.2",
+    "neostandard": "^0.11.4",
     "pre-commit": "^1.2.2",
     "proxyquire": "^2.1.3",
     "semver": "^7.5.0",
     "sinon": "^17.0.0",
-    "snazzy": "^9.0.0",
     "split2": "^4.0.0",
-    "standard": "^17.0.0",
     "tap": "^16.3.0",
     "tsd": "^0.30.0",
     "typescript": "^5.0.2",
     "wait-on": "^7.0.1"
   },
   "dependencies": {
-    "@fastify/error": "^3.0.0",
-    "@fastify/static": "^7.0.0",
-    "@fastify/websocket": "^10.0.0",
-    "fastify-plugin": "^4.2.0",
+    "@fastify/error": "^4.0.0",
+    "@fastify/static": "^8.0.0",
+    "@fastify/websocket": "^11.0.0",
+    "fastify-plugin": "^5.0.0",
     "graphql-jit": "0.8.4",
     "mqemitter": "^5.0.0",
     "p-map": "^4.0.0",
+    "quick-lru": "^7.0.0",
     "readable-stream": "^4.0.0",
     "safe-stable-stringify": "^2.3.0",
     "secure-json-parse": "^2.7.0",
@@ -77,6 +75,6 @@
     "directory": "test/types"
   },
   "engines": {
-    "node": ">=14.19.3"
+    "node": "^20.9.0 || >=22.0.0"
   }
 }


Test patch:
diff --git a/test/app-decorator.js b/test/app-decorator.js
index 13c7baf..8e9e197 100644
--- a/test/app-decorator.js
+++ b/test/app-decorator.js
@@ -4,6 +4,7 @@ const { test } = require('tap')
 const Fastify = require('fastify')
 const GQL = require('..')
 const { GraphQLError } = require('graphql')
+const gql = require('graphql-tag')
 
 const {
   GraphQLScalarType,
@@ -1439,3 +1440,33 @@ test('defineResolvers should support __resolveReference', async (t) => {
   // needed so that graphql is defined
   await app.ready()
 })
+
+test('graphql-tag', async (t) => {
+  const app = Fastify()
+  const schema = `
+    type Query {
+      add(x: Int, y: Int): Int
+    }
+  `
+
+  const resolvers = {
+    add: async ({ x, y }) => x + y
+  }
+
+  app.register(GQL, {
+    schema,
+    resolvers
+  })
+
+  // needed so that graphql is defined
+  await app.ready()
+
+  const query = gql`{ add(x: 2, y: 2) }`
+  const res = await app.graphql(query)
+
+  t.same(res, {
+    data: {
+      add: 4
+    }
+  })
+})
diff --git a/test/subscription-connection.js b/test/subscription-connection.js
index 2624421..461ec12 100644
--- a/test/subscription-connection.js
+++ b/test/subscription-connection.js
@@ -73,7 +73,7 @@ test('subscription connection sends error message when message is not json strin
       t.equal(JSON.stringify({
         type: 'error',
         id: null,
-        payload: 'Message must be a JSON string'
+        payload: [{ message: 'Message must be a JSON string' }]
       }), message)
     },
     protocol: GRAPHQL_TRANSPORT_WS
@@ -255,7 +255,7 @@ test('subscription connection send error message when GQL_START handler errs', a
       t.equal(JSON.stringify({
         type: 'error',
         id: 1,
-        payload: 'handleGQLStart error'
+        payload: [{ message: 'handleGQLStart error' }]
       }), message)
     },
     protocol: GRAPHQL_TRANSPORT_WS
@@ -284,7 +284,7 @@ test('subscription connection send error message when client message type is inv
       t.equal(JSON.stringify({
         type: 'error',
         id: 1,
-        payload: 'Invalid payload type'
+        payload: [{ message: 'Invalid payload type' }]
       }), message)
     },
     protocol: GRAPHQL_TRANSPORT_WS
@@ -305,7 +305,7 @@ test('subscription connection handles GQL_START message correctly, when payload.
     close () {},
     send (message) {
       t.equal(JSON.stringify(
-        { type: 'error', id: 1, payload: 'Must provide document.' }
+        { type: 'error', id: 1, payload: [{ message: 'Must provide document.' }] }
       ), message)
     },
     protocol: GRAPHQL_TRANSPORT_WS
@@ -440,7 +440,7 @@ test('subscription connection send GQL_ERROR message if connectionInit extension
       t.same(JSON.parse(message), {
         id: 1,
         type: 'error',
-        payload: 'Forbidden'
+        payload: [{ message: 'Forbidden' }]
       })
     },
     protocol: GRAPHQL_TRANSPORT_WS
@@ -509,7 +509,7 @@ test('subscription connection send GQL_ERROR on unknown extension', async (t) =>
       t.same(JSON.parse(message), {
         id: 1,
         type: 'error',
-        payload: 'Unknown extension unknown'
+        payload: [{ message: 'Unknown extension unknown' }]
       })
     },
     protocol: GRAPHQL_TRANSPORT_WS
@@ -584,7 +584,7 @@ test('subscription connection sends error when trying to execute invalid operati
           JSON.stringify({
             type: 'error',
             id: 1,
-            payload: 'Invalid operation: query'
+            payload: [{ message: 'Invalid operation: query' }]
           }),
           message
         )
diff --git a/test/subscription-hooks.js b/test/subscription-hooks.js
index fc158c8..fe0f87e 100644
--- a/test/subscription-hooks.js
+++ b/test/subscription-hooks.js
@@ -218,7 +218,7 @@ test('subscription - should handle preSubscriptionParsing hook errors', async t
     t.same(data, {
       id: 1,
       type: 'error',
-      payload: 'a preSubscriptionParsing error occurred'
+      payload: [{ message: 'a preSubscriptionParsing error occurred' }]
     })
   }
 })
@@ -268,7 +268,7 @@ test('subscription - should handle preSubscriptionExecution hook errors', async
     t.same(data, {
       id: 1,
       type: 'error',
-      payload: 'a preSubscriptionExecution error occurred'
+      payload: [{ message: 'a preSubscriptionExecution error occurred' }]
     })
   }
 })
diff --git a/test/subscription.js b/test/subscription.js
index 12d1c29..0edc92e 100644
--- a/test/subscription.js
+++ b/test/subscription.js
@@ -1049,7 +1049,11 @@ test('subscription server sends correct error if execution throws', t => {
         t.equal(chunk, JSON.stringify({
           type: 'error',
           id: 1,
-          payload: 'custom execution error'
+          payload: [{
+            message: 'custom execution error',
+            locations: [{ line: 3, column: 13 }],
+            path: ['notificationAdded']
+          }]
         }))
 
         client.end()
diff --git a/test/types/index.ts b/test/types/index.ts
index b941f5b..4e86398 100644
--- a/test/types/index.ts
+++ b/test/types/index.ts
@@ -1,11 +1,9 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions */
+
 import { expectAssignable, expectError, expectType } from 'tsd'
-/* eslint-disable no-unused-expressions */
 import { EventEmitter } from 'events'
-// eslint-disable-next-line no-unused-vars
 import Fastify, { FastifyRequest } from 'fastify'
-// eslint-disable-next-line no-unused-vars
 import { Readable } from 'stream'
-// eslint-disable-next-line no-unused-vars
 import * as mercuriusNamespaceImport from '../..'
 import mercurius, {
   mercurius as mercuriusNamedImport,
@@ -19,7 +17,6 @@ import mercurius, {
   MercuriusPlugin,
   MercuriusLoaders
 } from '../..'
-// eslint-disable-next-line no-unused-vars
 import { DocumentNode, ExecutionResult, GraphQLSchema, ValidationContext, ValidationRule } from 'graphql'
 import { mapSchema } from '@graphql-tools/utils'
 import mq from 'mqemitter'
@@ -62,7 +59,7 @@ const resolvers: IResolvers = {
 
 // declare module 'mercurius' {
 declare module '../../' {
-  interface MercuriusContext { // eslint-disable-line
+  interface MercuriusContext {
     request: FastifyRequest
   }
 }
@@ -139,7 +136,7 @@ app.register(mercurius, {
       }
     },
     Mutation: {
-      addDog (_root, { name, breed }: { name: string; breed?:string }, ctx) {
+      addDog (_root, { name, breed }: { name: string; breed?: string }, ctx) {
         ctx.pubsub.publish({
           topic: 'new_dog',
           payload: {
@@ -208,7 +205,7 @@ app.register(async function (app) {
   })
   app.graphql.defineLoaders({
     Dog: {
-      owner: async (queries: Array<{ obj: { name: keyof typeof owners }, params: {a: string} }>, _ctx) => {
+      owner: async (queries: Array<{ obj: { name: keyof typeof owners }, params: { a: string } }>, _ctx) => {
         return queries.map(({ obj }) => owners[obj.name])
       }
     }
@@ -580,7 +577,7 @@ expectError(() => {
 })
 
 declare module 'fastify' {
-// eslint-disable-next-line no-unused-vars
+
   interface FastifyInstance {
     graphql: MercuriusPlugin
   }