Skip to content

Commit

Permalink
send empty data for complete event on sse stream (#3214)
Browse files Browse the repository at this point in the history
* send empty data for complete event on sse stream

* update snapshots

* more details in changeset

* update snapshots
  • Loading branch information
n1ru4l committed Mar 28, 2024
1 parent c79dc29 commit f89a1aa
Show file tree
Hide file tree
Showing 12 changed files with 119 additions and 77 deletions.
5 changes: 5 additions & 0 deletions .changeset/weak-trees-grow.md
@@ -0,0 +1,5 @@
---
'graphql-yoga': patch
---

Always include empty data payload for final `complete` event of SSE stream responses to ensure [`EventSource`](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) compatibility. See the [GraphQL over SSE protocol](https://github.com/enisdenjo/graphql-sse/blob/master/PROTOCOL.md#complete-event) for more information.
102 changes: 50 additions & 52 deletions examples/fastify/__integration-tests__/fastify.spec.ts
Expand Up @@ -139,43 +139,42 @@ describe('fastify example integration', () => {
});
expect(response.statusCode).toEqual(200);
expect(response.text.replace(/:\n\n/g, '')).toMatchInlineSnapshot(`
"event: next
data: {"data":{"countdown":10}}
"event: next
data: {"data":{"countdown":10}}
event: next
data: {"data":{"countdown":9}}
event: next
data: {"data":{"countdown":9}}
event: next
data: {"data":{"countdown":8}}
event: next
data: {"data":{"countdown":8}}
event: next
data: {"data":{"countdown":7}}
event: next
data: {"data":{"countdown":7}}
event: next
data: {"data":{"countdown":6}}
event: next
data: {"data":{"countdown":6}}
event: next
data: {"data":{"countdown":5}}
event: next
data: {"data":{"countdown":5}}
event: next
data: {"data":{"countdown":4}}
event: next
data: {"data":{"countdown":4}}
event: next
data: {"data":{"countdown":3}}
event: next
data: {"data":{"countdown":3}}
event: next
data: {"data":{"countdown":2}}
event: next
data: {"data":{"countdown":2}}
event: next
data: {"data":{"countdown":1}}
event: next
data: {"data":{"countdown":1}}
event: next
data: {"data":{"countdown":0}}
event: next
data: {"data":{"countdown":0}}
event: complete
"
`);
event: complete
data"
`);
});
it('handles subscription operations via POST', async () => {
const response = await request(app.server)
Expand All @@ -193,43 +192,42 @@ describe('fastify example integration', () => {
});
expect(response.statusCode).toEqual(200);
expect(response.text.replace(/:\n\n/g, '')).toMatchInlineSnapshot(`
"event: next
data: {"data":{"countdown":10}}
event: next
data: {"data":{"countdown":9}}
"event: next
data: {"data":{"countdown":10}}
event: next
data: {"data":{"countdown":8}}
event: next
data: {"data":{"countdown":9}}
event: next
data: {"data":{"countdown":7}}
event: next
data: {"data":{"countdown":8}}
event: next
data: {"data":{"countdown":6}}
event: next
data: {"data":{"countdown":7}}
event: next
data: {"data":{"countdown":5}}
event: next
data: {"data":{"countdown":6}}
event: next
data: {"data":{"countdown":4}}
event: next
data: {"data":{"countdown":5}}
event: next
data: {"data":{"countdown":3}}
event: next
data: {"data":{"countdown":4}}
event: next
data: {"data":{"countdown":2}}
event: next
data: {"data":{"countdown":3}}
event: next
data: {"data":{"countdown":1}}
event: next
data: {"data":{"countdown":2}}
event: next
data: {"data":{"countdown":0}}
event: next
data: {"data":{"countdown":1}}
event: complete
event: next
data: {"data":{"countdown":0}}
"
`);
event: complete
data"
`);
});
it('should handle file uploads', async () => {
const response = await request(app.server)
Expand Down
Expand Up @@ -82,6 +82,7 @@ event: next
data: {"data":{"requiresAuth":"hi foo@foo.com"}}
event: complete
data:
"
`);
Expand All @@ -103,6 +104,7 @@ event: next
data: {"data":null,"errors":[{"message":"Accessing 'Subscription.requiresAuth' requires authentication.","locations":[{"line":1,"column":14}]}]}
event: complete
data:
"
`);
Expand Down
1 change: 1 addition & 0 deletions examples/hapi/__integration-tests__/hapi.spec.ts
Expand Up @@ -76,6 +76,7 @@ event: next
data: {"data":{"greetings":"Zdravo"}}
event: complete
data:
"
`);
Expand Down
1 change: 1 addition & 0 deletions examples/node-ts/__integration-tests__/node-ts.spec.ts
Expand Up @@ -25,6 +25,7 @@ event: next
data: {"errors":[{"message":"Subscriptions have been disabled"}]}
event: complete
data:
"
`);
Expand Down
1 change: 1 addition & 0 deletions examples/pothos/__integration-tests__/pothos.spec.ts
Expand Up @@ -35,6 +35,7 @@ event: next
data: {"data":{"greetings":"Zdravo"}}
event: complete
data:
"
`);
Expand Down
38 changes: 31 additions & 7 deletions packages/graphql-yoga/__integration-tests__/browser.spec.ts
Expand Up @@ -119,8 +119,32 @@ export function createTestSchema() {
}),
resolve: value => value,
},
countdown: {
type: new GraphQLNonNull(GraphQLInt),
args: {
from: {
type: new GraphQLNonNull(GraphQLInt),
},
},
subscribe: (_, { from }) =>
new Repeater((push, end) => {
let counter = from;
const send = () => {
push(counter);
if (counter === 0) {
end();
}

counter--;
};
const interval = setInterval(() => send(), 1000);
end.then(() => clearInterval(interval));
}),
resolve: value => value,
},
error: {
type: GraphQLBoolean,
// eslint-disable-next-line require-yield
async *subscribe() {
throw new Error('This is not okay');
},
Expand Down Expand Up @@ -650,7 +674,7 @@ describe('browser', () => {
'query',
/* GraphQL */ `
subscription {
counter
countdown(from: 1)
}
`,
);
Expand All @@ -662,10 +686,10 @@ describe('browser', () => {
const values: Array<string> = [];
source.addEventListener('next', event => {
values.push(event.data);
if (values.length === 2) {
res({ data: values });
source.close();
}
});
source.addEventListener('complete', () => {
source.close();
res({ data: values });
});
source.onerror = err => {
res({ error: String(err) });
Expand All @@ -679,8 +703,8 @@ describe('browser', () => {

expect(result.data).toMatchInlineSnapshot(`
[
"{"data":{"counter":0}}",
"{"data":{"counter":1}}",
"{"data":{"countdown":1}}",
"{"data":{"countdown":0}}",
]
`);
});
Expand Down
Expand Up @@ -374,10 +374,11 @@ describe('incremental delivery: node-fetch', () => {

chunk = await reader.read();
expect(Buffer.from(chunk.value!).toString('utf-8')).toMatchInlineSnapshot(`
"event: complete
"event: complete
data:
"
`);
"
`);

chunk = await reader.read();
expect(chunk.done).toBeTruthy();
Expand Down
2 changes: 2 additions & 0 deletions packages/graphql-yoga/__tests__/graphql-sse.spec.ts
Expand Up @@ -61,6 +61,7 @@ describe('GraphQL over SSE', () => {
:
event: complete
data:
"
`);
Expand Down Expand Up @@ -178,6 +179,7 @@ event: next
data: {"errors":[{"message":"Cannot query field \\"nope\\" on type \\"Query\\".","locations":[{"line":1,"column":2}]}]}
event: complete
data:
"
`);
Expand Down
32 changes: 18 additions & 14 deletions packages/graphql-yoga/__tests__/subscriptions.spec.ts
Expand Up @@ -122,25 +122,26 @@ describe('Subscription', () => {
}

expect(results).toMatchInlineSnapshot(`
[
":
[
":
",
":
",
":
",
":
",
":
",
"event: next
data: {"data":{"hi":"hi"}}
",
"event: next
data: {"data":{"hi":"hi"}}
",
"event: complete
",
"event: complete
data:
",
]
`);
",
]
`);
});

test('should issue pings event if event source never publishes anything', async () => {
Expand Down Expand Up @@ -284,6 +285,7 @@ event: next
data: {"errors":[{"message":"Unexpected error.","locations":[{"line":2,"column":11}]}]}
event: complete
data:
"
`);
Expand Down Expand Up @@ -352,6 +354,7 @@ event: next
data: {"errors":[{"message":"hi","locations":[{"line":2,"column":11}]}]}
event: complete
data:
"
`);
Expand Down Expand Up @@ -415,6 +418,7 @@ event: next
data: {"errors":[{"message":"hi","locations":[{"line":2,"column":11}]}]}
event: complete
data:
"
`);
Expand Down
3 changes: 2 additions & 1 deletion packages/graphql-yoga/src/plugins/result-processor/sse.ts
Expand Up @@ -65,7 +65,8 @@ export function getSSEProcessor(): ResultProcessor {
controller.enqueue(textEncoder.encode(`data: ${chunk}\n\n`));
}
if (done) {
controller.enqueue(textEncoder.encode(`event: complete\n\n`));
controller.enqueue(textEncoder.encode(`event: complete\n`));
controller.enqueue(textEncoder.encode(`data:\n\n`));
clearInterval(pingInterval);
controller.close();
}
Expand Down
2 changes: 2 additions & 0 deletions packages/nestjs/__tests__/subscriptions.spec.ts
Expand Up @@ -50,6 +50,7 @@ event: next
data: {"data":{"greetings":"Zdravo"}}
event: complete
data:
"
`);
Expand Down Expand Up @@ -80,6 +81,7 @@ event: next
data: {"data":{"filteredGreetings":"Hola"}}
event: complete
data:
"
`);
Expand Down

0 comments on commit f89a1aa

Please sign in to comment.