Skip to content

Commit a499eec

Browse files
stdeviStanislav Deviatykhclaude
authored
feat(graphql): reply with pong when the server sends a ping (#878)
* feat(graphql): reply with pong when the server sends a ping The graphql-transport-ws protocol defines ping/pong as bidirectional and requires the receiving party to answer a ping as soon as possible. The client only ever sent pings, so a server-initiated ping fell through to 'unrecognized' and was silently dropped. Inert on the legacy endpoint, which never initiates a ping (its heartbeat is 'ka'); this prepares the client for /graphql/subscription-v2, whose handler is configured with a 10s keep-alive that pings idle connections. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Update graphql.service.ts * Update subscription-protocol.ts --------- Co-authored-by: Stanislav Deviatykh <stanislav@qminder.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 5124ad2 commit a499eec

4 files changed

Lines changed: 36 additions & 0 deletions

File tree

packages/javascript-api/src/lib/services/graphql/__tests__/graphql-subscriptions.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,19 @@ describe('GraphQL subscriptions', () => {
245245
subscription.unsubscribe();
246246
});
247247

248+
it('replies with a pong when the server sends a ping', async () => {
249+
const subscription = fixture.triggerSubscription();
250+
251+
await fixture.handleConnectionInit();
252+
await fixture.consumeSubscribeMessage();
253+
254+
fixture.sendMessageToClient({ type: 'ping' });
255+
256+
expect(await fixture.getNextMessage()).toEqual({ type: 'pong' });
257+
258+
subscription.unsubscribe();
259+
});
260+
248261
it('when the server sends an error, it will reconnect and subscribe again', async () => {
249262
const subscription = fixture.triggerSubscription();
250263
useFakeSetInterval();

packages/javascript-api/src/lib/services/graphql/__tests__/subscription-protocol.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ describe('LegacySubscriptionProtocol', () => {
3232
it('serializes ping without id and payload', () => {
3333
expect(JSON.parse(protocol.serializePing())).toEqual({ type: 'ping' });
3434
});
35+
36+
it('serializes pong without id and payload', () => {
37+
expect(JSON.parse(protocol.serializePong())).toEqual({ type: 'pong' });
38+
});
3539
});
3640

3741
describe('incoming frames', () => {
@@ -47,6 +51,12 @@ describe('LegacySubscriptionProtocol', () => {
4751
).toEqual({ type: 'connection-ack' });
4852
});
4953

54+
it('parses a server-sent ping', () => {
55+
expect(protocol.parseIncomingMessage('{"type":"ping"}')).toEqual({
56+
type: 'ping',
57+
});
58+
});
59+
5060
it('parses pong', () => {
5161
expect(protocol.parseIncomingMessage('{"type":"pong"}')).toEqual({
5262
type: 'pong',

packages/javascript-api/src/lib/services/graphql/graphql.service.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,10 @@ export class GraphqlService {
542542
break;
543543
}
544544

545+
case 'ping':
546+
this.sendRawMessage(this.protocol.serializePong());
547+
break;
548+
545549
case 'pong':
546550
clearTimeout(this.pongTimeout);
547551
this.connectionAttemptsCount = 0;

packages/javascript-api/src/lib/services/graphql/subscription-protocol.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface QminderGraphQLError {
1717
export type IncomingSubscriptionMessage =
1818
| { readonly type: 'connection-ack' }
1919
| { readonly type: 'keep-alive' }
20+
| { readonly type: 'ping' }
2021
| { readonly type: 'pong' }
2122
| {
2223
readonly type: 'data';
@@ -45,6 +46,7 @@ export interface SubscriptionProtocol {
4546
serializeSubscribe(id: string, query: string): string;
4647
serializeUnsubscribe(id: string): string;
4748
serializePing(): string;
49+
serializePong(): string;
4850
parseIncomingMessage(data: string): IncomingSubscriptionMessage;
4951
}
5052

@@ -106,6 +108,10 @@ export class LegacySubscriptionProtocol implements SubscriptionProtocol {
106108
return JSON.stringify({ type: LegacyMessageType.GQL_PING });
107109
}
108110

111+
serializePong(): string {
112+
return JSON.stringify({ type: LegacyMessageType.GQL_PONG });
113+
}
114+
109115
parseIncomingMessage(data: string): IncomingSubscriptionMessage {
110116
const message: LegacyMessage = JSON.parse(data);
111117

@@ -116,6 +122,9 @@ export class LegacySubscriptionProtocol implements SubscriptionProtocol {
116122
case LegacyMessageType.GQL_CONNECTION_ACK:
117123
return { type: 'connection-ack' };
118124

125+
case LegacyMessageType.GQL_PING:
126+
return { type: 'ping' };
127+
119128
case LegacyMessageType.GQL_PONG:
120129
return { type: 'pong' };
121130

0 commit comments

Comments
 (0)