Search code examples
websocketgraphqlkeystonejsgraphql-subscriptionskeystonejs6

keystone.js graphql subscriptions not working - code: 4406, reason: "Subprotocol not acceptable"


I am developing a keystone app, where we want to use graphql subscriptions.

Followed the documentation: https://keystonejs.com/docs/config/config#extend-http-server

and the keytone github example for websocket file: https://github.com/keystonejs/keystone/blob/main/examples/extend-graphql-subscriptions/websocket.ts

The created subscriptions appear in the Apollo playground schema.

The websocket connection works as long as I comment out graphql-ws from the code. When I add wsUseServer coming from graphql-ws it gives the following errors on these two tries:

  • wscat -c ws://localhost:3000/api/graphql >
    Connected (press CTRL+C to quit) Disconnected (code: 4406, reason: "Subprotocol not acceptable")
  • wscat -c ws://localhost:3000/api/graphql -s graphql-ws error: Server sent no subprotocol

My websocket.ts code which is the same as the example, excepting logs and commented test lines for debugging:


import type http from 'http'
import { useServer as wsUseServer } from 'graphql-ws/lib/use/ws'
import { WebSocketServer } from 'ws'
import { PubSub } from 'graphql-subscriptions'
import { parse } from 'graphql'
import { type Context } from '.keystone/types'

// Setup pubsub as a Global variable in dev so it survives Hot Reloads.
declare global {
  let graphqlSubscriptionPubSub: PubSub
}

// The 'graphql-subscriptions' pubsub library is not recommended for production use, but can be useful as an example
//   for details see https://www.apollographql.com/docs/apollo-server/data/subscriptions/#the-pubsub-class
export const pubSub = global.graphqlSubscriptionPubSub || new PubSub()
globalThis.graphqlSubscriptionPubSub = pubSub

export function extendHttpServer (
  httpServer: http.Server,
  commonContext: Context,
) {
  console.log('HTTP Server:', httpServer);  
  console.log(commonContext.graphql.schema)

  // Setup WebSocket server using 'ws'
  const wss = new WebSocketServer({
    server: httpServer,
    path: '/api/graphql',
    /*handleProtocols: (protocols) => {
      return Array.from(protocols).includes('graphql-ws') ? 'graphql-ws' : null;
    }*/
  })

  // Setup the WebSocket to handle GraphQL subscriptions using 'graphql-ws'
  wsUseServer(
    {
      schema: commonContext.graphql.schema,
      // run these onSubscribe functions as needed or remove them if you don't need them
      onSubscribe: async (ctx: any, msg) => {
        const context = await commonContext.withRequest(ctx.extra.request)
        // Return the execution args for this subscription passing through the Keystone Context
        return {
          schema: commonContext.graphql.schema,
          operationName: msg.payload.operationName,
          document: parse(msg.payload.query),
          variableValues: msg.payload.variables,
          contextValue: context,
        }
      },
    },
    wss
  )

  //wsUseServer({ schema: commonContext.graphql.schema }, wss); // For testing
  //const ws = new WebSocket('ws://localhost:3000/api/graphql', 'graphql-ws'); // For testing

  // Optional: Log connection events
  wss.on('connection', (ws, request) => {
    console.log('New WebSocket connection');
    console.log('Requested Protocols:', request.headers['sec-websocket-protocol']);
    console.log('Selected Protocol:', ws.protocol); // Log the subprotocol selected by the server    

    ws.on('close', (error) => {
      console.error('WebSocket error:', error);
    });    
    
    ws.on('message', (message) => {
      console.log('Received message:', message);
    });

    ws.on('error', (error) => {
      console.error('WebSocket error:', error);
    });
    ws.onerror = (evt) => {
  console.log('Message received:', evt);
};
  });

  // Send the time every second as an interval example of pub/sub
  /*setInterval(() => {
    console.log('TIME', Date.now())
    pubSub.publish('TIME', {
      time: {
        iso: new Date().toISOString(),
      },
    })
  }, 1000)*/
}

I removed wsUseServer (Graphql Subscriptions) and the connection is maintained. But what I want is to use the subscriptions. SetInterval (commented out above) is working upon connection.

I added [handleProtocols] to the websocketserver (commented out above). Didn't solve.

Tested both wscat entries in development and production environment and the behavior is the same on both.

What I expect is to be able to maintain the websocket connection with the graphql-ws wsUseServer, to be able to activate subscriptions.

Thanks.


Solution

  • Figured out the solution. Firstly wscat is not the right tool for testing this. Secondly the right protocol is not graphql-ws, but graphql-transport-ws. It is not necessary to specify, since it is handled automatically.

    I tested the subscriptions with the Apollo playground and they showed as listening but wouldn't respond.

    The issue was with the way Pubsub was implemented. I was calling it in every file I had to use it, and it would generate conflict. It has to be launched in a single file, and than exported to every file in which one wants to use it.