Skip to content
Open
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
19 changes: 18 additions & 1 deletion src/StrawberryShake/Client/src/Transport.Http/HttpConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ public class HttpConnection : IHttpConnection
public const string RequestUri = "StrawberryShake.Transport.Http.HttpConnection.RequestUri";
public const string HttpClient = "StrawberryShake.Transport.Http.HttpConnection.HttpClient";

// Blazor WASM buffers the entire response body by default, so SSE subscription events only
// surface once the connection closes. Setting this per-request option opts into response
// streaming so events surface as they arrive. The option is ignored on non-WASM handlers
// and is the default on .NET 10+.
private static readonly HttpRequestOptionsKey<bool> s_enableWasmResponseStreaming =
new("WebAssemblyEnableStreamingResponse");

private static readonly OnHttpRequestMessageCreated s_enableResponseStreaming =
static (_, message, _) => message.Options.Set(s_enableWasmResponseStreaming, true);

private readonly Func<OperationRequest, object?, HttpClient> _createClient;
private readonly object? _clientFactoryState;

Expand Down Expand Up @@ -103,7 +113,14 @@ protected virtual GraphQLHttpRequest CreateHttpRequest(OperationRequest request)
operation = new HotChocolate.Transport.OperationRequest(body, null, name, onError: null, variables, extensions);
}

return new GraphQLHttpRequest(operation) { EnableFileUploads = hasFiles };
var httpRequest = new GraphQLHttpRequest(operation) { EnableFileUploads = hasFiles };

if (document.Kind == OperationKind.Subscription)
{
httpRequest.OnMessageCreated = s_enableResponseStreaming;
}

return httpRequest;
}

protected virtual Response<JsonDocument> CreateResponse(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Text;
using System.Text.Json;

namespace StrawberryShake.Transport.Http;

public class HttpConnectionStreamingTests
{
private static readonly HttpRequestOptionsKey<bool> s_streamingOption =
new("WebAssemblyEnableStreamingResponse");

[Fact]
public async Task CreateHttpRequest_Should_EnableResponseStreaming_When_OperationIsSubscription()
{
// arrange
var handler = new CapturingHandler(
"text/event-stream",
"event: next\ndata: {\"data\":{}}\n\nevent: complete\n\n");
var client = new HttpClient(handler) { BaseAddress = new Uri("http://localhost/graphql") };

var document = new TestDocument(OperationKind.Subscription, "subscription Test { __typename }");
var request = new OperationRequest("Test", document);

// act
var connection = new HttpConnection(() => client);
await foreach (var _ in connection.ExecuteAsync(request))
{
}

// assert
Assert.NotNull(handler.CapturedRequest);
var found = handler.CapturedRequest!.Options.TryGetValue(s_streamingOption, out var enabled);
Assert.True(found);
Assert.True(enabled);
}

[Fact]
public async Task CreateHttpRequest_Should_NotEnableResponseStreaming_When_OperationIsQuery()
{
// arrange
var handler = new CapturingHandler(
"application/graphql-response+json",
"{\"data\":{}}");
var client = new HttpClient(handler) { BaseAddress = new Uri("http://localhost/graphql") };

var document = new TestDocument(OperationKind.Query, "query Test { __typename }");
var request = new OperationRequest("Test", document);

// act
var connection = new HttpConnection(() => client);
await foreach (var _ in connection.ExecuteAsync(request))
{
}

// assert
Assert.NotNull(handler.CapturedRequest);
var found = handler.CapturedRequest!.Options.TryGetValue(s_streamingOption, out _);
Assert.False(found);
}

private sealed class CapturingHandler : HttpMessageHandler
{
private readonly string _contentType;
private readonly string _body;

public CapturingHandler(string contentType, string body)
{
_contentType = contentType;
_body = body;
}

public HttpRequestMessage? CapturedRequest { get; private set; }

protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
CapturedRequest = request;

var content = new ByteArrayContent(Encoding.UTF8.GetBytes(_body));
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(_contentType);

var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = content };
return Task.FromResult(response);
}
}

private sealed class TestDocument : IDocument
{
private readonly byte[] _query;

public TestDocument(OperationKind kind, [StringSyntax("graphql")] string query)
{
Kind = kind;
_query = Encoding.UTF8.GetBytes(query);
}

public OperationKind Kind { get; }

public ReadOnlySpan<byte> Body => _query;

public DocumentHash Hash { get; } = new("MD5", "ABC");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using HotChocolate.StarWars.Models;
using HotChocolate.Subscriptions;
using Microsoft.Extensions.DependencyInjection;
// TestServerHelper lives in the WebSocket test project's namespace; it only hosts the GraphQL
// server here. This test runs subscriptions over SSE (HTTP), so no WebSocket client is registered.
using StrawberryShake.Transport.WebSockets;
using static HotChocolate.StarWars.Types.Subscriptions;

Expand All @@ -26,9 +28,6 @@ public async Task Execute_StarWarsOnReviewSubGraphQLSSE_Test()
serviceCollection.AddHttpClient(
StarWarsOnReviewSubGraphQLSSEClient.ClientName,
c => c.BaseAddress = new Uri("http://localhost:" + port + "/graphql"));
serviceCollection.AddWebSocketClient(
StarWarsOnReviewSubGraphQLSSEClient.ClientName,
c => c.Uri = new Uri("ws://localhost:" + port + "/graphql"));
serviceCollection.AddStarWarsOnReviewSubGraphQLSSEClient();
IServiceProvider services = serviceCollection.BuildServiceProvider();
var client = services.GetRequiredService<StarWarsOnReviewSubGraphQLSSEClient>();
Expand Down
Loading