Repository: mcollina/fastify-gql
Pull request #267 (2020-08-28 10:40:17)

Fix websocket connection issues with postgraphile
Some backend federated services like postgraphile.org send periodic keepalive message over websocket.

This fixes compatibility with such graphql servers.

heri16: Should be GQL_CONNECTION_KEEP_ALIVE

The complete list is here: https://github.com/apollographql/subscriptions-transport-ws/blob/faa219cff7b6f9873cae59b490da46684d7bea19/src/message-types.ts#L7
mcollina: this would also need a rebase

Patch:
diff --git a/lib/subscription-client.js b/lib/subscription-client.js
index 5f44ac8..05580f2 100644
--- a/lib/subscription-client.js
+++ b/lib/subscription-client.js
@@ -5,6 +5,7 @@ const {
   GQL_CONNECTION_INIT,
   GQL_CONNECTION_ACK,
   GQL_CONNECTION_ERROR,
+  GQL_CONNECTION_KEEP_ALIVE,
   GQL_START,
   GQL_DATA,
   GQL_COMPLETE,
@@ -143,7 +144,7 @@ class SubscriptionClient {
     }
   }
 
-  sendMessage (operationId, type, payload) {
+  sendMessage (operationId, type, payload = {}) {
     this.socket.send(
       JSON.stringify({
         id: operationId,
@@ -204,6 +205,8 @@ class SubscriptionClient {
       case GQL_CONNECTION_ERROR:
         this.close(this.tryReconnect, false)
         break
+      case GQL_CONNECTION_KEEP_ALIVE:
+        break
       /* istanbul ignore next */
       default:
         /* istanbul ignore next */
diff --git a/lib/subscription-protocol.js b/lib/subscription-protocol.js
index 1dbdb01..e93f5d4 100644
--- a/lib/subscription-protocol.js
+++ b/lib/subscription-protocol.js
@@ -3,6 +3,7 @@
 module.exports.GQL_CONNECTION_INIT = 'connection_init' // Client -> Server
 module.exports.GQL_CONNECTION_ACK = 'connection_ack' // Server -> Client
 module.exports.GQL_CONNECTION_ERROR = 'connection_error' // Server -> Client
+module.exports.GQL_CONNECTION_KEEP_ALIVE = 'ka' // Server -> Client
 
 module.exports.GQL_CONNECTION_TERMINATE = 'connection_terminate' // Client -> Server
 module.exports.GQL_START = 'start' // Client -> Server


Test patch:
diff --git a/test/subscription-client.js b/test/subscription-client.js
index 216c96e..f805606 100644
--- a/test/subscription-client.js
+++ b/test/subscription-client.js
@@ -1,5 +1,8 @@
 'use strict'
 const { test } = require('tap')
+
+const FakeTimers = require('@sinonjs/fake-timers')
+
 const SubscriptionClient = require('../lib/subscription-client')
 const WS = require('ws')
 
@@ -289,3 +292,77 @@ test('subscription client closes the connection if connectionInitPayload throws'
     }
   })
 })
+
+test('subscription client sending empty object payload on connection init', (t) => {
+  const server = new WS.Server({ port: 0 })
+  const port = server.address().port
+
+  server.on('connection', function connection (ws) {
+    ws.on('message', function incoming (message) {
+      const data = JSON.parse(message)
+      if (data.type === 'connection_init') {
+        t.deepEqual(data.payload, {})
+        ws.send(JSON.stringify({ id: '1', type: 'complete' }))
+      }
+    })
+
+    ws.send(JSON.stringify({ id: undefined, type: 'connection_ack' }))
+  })
+
+  const maxReconnectAttempts = 10
+  const client = new SubscriptionClient(`ws://localhost:${port}`, {
+    reconnect: true,
+    maxReconnectAttempts,
+    serviceName: 'test-service'
+  })
+
+  client.createSubscription('query', {}, (data) => {
+    client.close()
+    server.close()
+    t.end()
+  })
+})
+
+test('subscription client not throwing error on GQL_CONNECTION_KEEP_ALIVE type payload received', (t) => {
+  const clock = FakeTimers.createClock()
+  const server = new WS.Server({ port: 0 })
+  const port = server.address().port
+  t.tearDown(() => {
+    server.close()
+  })
+
+  server.on('connection', function connection (ws) {
+    ws.on('message', function incoming (message) {
+      const data = JSON.parse(message)
+      if (data.type === 'start') {
+        ws.send(JSON.stringify({ id: '1', type: 'complete' }))
+      }
+    })
+
+    ws.send(JSON.stringify({ id: undefined, type: 'connection_ack' }))
+
+    clock.setInterval(() => {
+      ws.send(JSON.stringify({ type: 'ka' }))
+    }, 200)
+  })
+
+  const client = new SubscriptionClient(`ws://localhost:${port}`, {
+    reconnect: true,
+    maxReconnectAttempts: 10,
+    serviceName: 'test-service',
+    connectionCallback: () => {
+      clock.tick(200)
+      clock.tick(200)
+    }
+  })
+
+  client.createSubscription('query', {}, (data) => {
+    t.deepEqual(data, {
+      topic: 'test-service_1',
+      payload: null
+    })
+    clock.tick(200)
+    client.close()
+    t.end()
+  })
+})