Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ public void Dispose()
private static class MessageHelper
{
private static ReadOnlySpan<byte> NextEvent => "event: next\ndata: "u8;
private static ReadOnlySpan<byte> CompleteEvent => "event: complete\n\n"u8;
private static ReadOnlySpan<byte> CompleteEvent => "event: complete\ndata:\n\n"u8;
private static ReadOnlySpan<byte> NewLine2 => "\n\n"u8;

public static void FormatNextMessage(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ ... @defer {
data: {"incremental":[{"id":"2","data":{"description":"Abc desc"}}],"completed":[{"id":"2"}],"hasNext":false}

event: complete
data:


""");
Expand Down Expand Up @@ -349,6 +350,7 @@ ... @defer {
data: {"incremental":[{"data":{"description":"Abc desc"},"path":["product"]}],"hasNext":false}

event: complete
data:


""");
Expand Down Expand Up @@ -401,6 +403,7 @@ ... @defer(label: "productDescription") {
data: {"incremental":[{"data":{"description":"Abc desc"},"path":["product"],"label":"productDescription"}],"hasNext":false}

event: complete
data:


""");
Expand Down Expand Up @@ -455,6 +458,7 @@ ... @defer(label: "productDescription") {
data: {"incremental":[{"id":"2","data":{"description":"Abc desc"}}],"completed":[{"id":"2"}],"hasNext":false}

event: complete
data:


""");
Expand Down Expand Up @@ -509,6 +513,7 @@ ... on Droid @defer(label: "droid_details") {
data: {"incremental":[{"id":"2","data":{"primaryFunction":"Astromech"}}],"completed":[{"id":"2"}],"hasNext":false}

event: complete
data:


""");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ public async Task SingleResult_Should_WriteOneNextEvent_When_EventStreamIsAccept
data: {"data":{"__typename":"Query"}}

event: complete
data:


""");
Expand Down Expand Up @@ -421,6 +422,7 @@ public async Task EventStream_Sends_KeepAlive()
:

event: complete
data:


""");
Expand Down Expand Up @@ -471,6 +473,7 @@ public async Task EventStream_When_Accept_Is_All()
:

event: complete
data:


""");
Expand Down Expand Up @@ -521,6 +524,7 @@ public async Task EventStream_When_Accept_Is_All_And_Subscription_Directive()
:

event: complete
data:


""");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ public async Task EventStream_Formats_New_Defer_Structure()
data: {{Payload2New}}

event: complete
data:
"""),
content);
}
Expand All @@ -104,6 +105,7 @@ public async Task EventStream_Formats_Legacy_Defer_Structure()
data: {{Payload2Legacy}}

event: complete
data:
"""),
content);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2010,6 +2010,54 @@ public async Task ReadAsResultStream_Text_Event_Stream_Unknown_Event_With_Data_I
Assert.Equal(0, count);
}

[Fact]
public async Task ReadAsResultStream_Text_Event_Stream_Complete_With_Empty_Data_Ends_Stream()
{
// arrange
// A complete event with an empty data field ends the stream before the next event.
var ms = new MemoryStream();
var sw = new StreamWriter(ms);
sw.Write("event: next");
sw.Write('\n');
sw.Write("data: {\"data\":{\"number\":0}}");
sw.Write('\n');
sw.Write('\n');
sw.Write("event: complete");
sw.Write('\n');
sw.Write("data:");
sw.Write('\n');
sw.Write('\n');
sw.Write("event: next");
sw.Write('\n');
sw.Write("data: {\"data\":{\"number\":1}}");
sw.Write('\n');
sw.Write('\n');
sw.Flush();
ms.Position = 0;

var handler = new MockHttpMessageHandler(ms, "text/event-stream");
using var client = new DefaultGraphQLHttpClient(new HttpClient(handler));

var operationRequest = new OperationRequest("{ number }");
var request = new GraphQLHttpRequest(
operationRequest,
new Uri("http://localhost:5000/graphql"));

// act
using var result = await client.SendAsync(request, TestContext.Current.CancellationToken);
var stream = result.ReadAsResultStreamAsync();

// assert
var numbers = new List<int>();

await foreach (var document in stream)
{
numbers.Add(document.Data.GetProperty("number").GetInt32());
}

Assert.Equal(0, Assert.Single(numbers));
}

[Fact]
public async Task Post_Variables_Do_Not_Escape_Apostrophe_To_Unicode()
{
Expand Down
1 change: 1 addition & 0 deletions website/content/docs/fusion/subscriptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,7 @@ event: next
data: {"data":{"onReviewAdded":{"body":"Works as described"}}}

event: complete
data:
```

For details on defining subscriptions in a subgraph (the `[Subscribe]` and `[Topic]`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1687,3 +1687,14 @@ The default is **64**. Operations that arrive while the gate is full queue up an
Every execution is bounded by the `ExecutionTimeout` option (default 30 seconds). This applies uniformly to queries, mutations, subscription handshakes, and each subscription event. The budget covers both the time an execution spends waiting for a concurrency slot and the time it spends running. When the budget is exceeded, the execution is cancelled and the caller receives a clean timeout error. `ExecutionTimeout` is the single setting that controls cancellation for every execution.

Subscriptions participate in the limit like any other operation. The initial subscribe consumes a slot while the subscribe resolver runs, and each emitted event consumes a slot while its result is being produced. Idle subscriptions (waiting on the next event) cost nothing. The slot is released between events.

## SSE complete event carries an empty data field

Hot Chocolate 16.0 through 16.6.7 ended every `text/event-stream` response with an `event: complete` message that had no `data:` field. The browser `EventSource` API does not dispatch such an event, so an `EventSource` client that did not close the connection itself reconnected when the response ended, and the server executed the operation again. The message now carries an empty `data:` field, and `EventSource` receives `complete`. This change lands in **16.6.8**.

Tests that compare SSE response bodies as strings expect the extra line:

```text
event: complete
data:
```
4 changes: 2 additions & 2 deletions website/content/docs/hotchocolate/server/http-transport.md
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ Results are delivered as SSE events. This transport works well with browser `Eve
Accept: text/event-stream
```

Each result is sent as an `event: next` message with the JSON payload in the `data:` field. A final `event: complete` message signals the end of the stream.
Each result is sent as an `event: next` message with the JSON payload in the `data:` field. A final `event: complete` message with an empty `data:` field signals the end of the stream.

## JSON Lines (`application/jsonl`)

Expand Down Expand Up @@ -520,7 +520,7 @@ event: complete
data:
```

Each result is delivered as an `event: next` message with the JSON payload in the `data:` field. A final `event: complete` message signals the end of the stream.
Each result is delivered as an `event: next` message with the JSON payload in the `data:` field. A final `event: complete` message with an empty `data:` field signals the end of the stream.

## SSE for Single Results

Expand Down
Loading