Skip to content

Commit 664aa6d

Browse files
committed
Disable buffering and add byte[] handling
1 parent 6f1f6be commit 664aa6d

File tree

2 files changed

+84
-0
lines changed

2 files changed

+84
-0
lines changed

src/Http/Http.Results/src/ServerSentEventsResult.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Microsoft.Extensions.Options;
1111
using Microsoft.AspNetCore.Http.Json;
1212
using Microsoft.Extensions.DependencyInjection;
13+
using Microsoft.AspNetCore.Http.Features;
1314

1415
namespace Microsoft.AspNetCore.Http.HttpResults;
1516

@@ -42,6 +43,10 @@ public async Task ExecuteAsync(HttpContext httpContext)
4243
httpContext.Response.ContentType = "text/event-stream";
4344
httpContext.Response.Headers.CacheControl = "no-cache,no-store";
4445
httpContext.Response.Headers.Pragma = "no-cache";
46+
httpContext.Response.Headers.ContentEncoding = "identity";
47+
48+
var bufferingFeature = httpContext.Features.GetRequiredFeature<IHttpResponseBodyFeature>();
49+
bufferingFeature.DisableBuffering();
4550

4651
var jsonOptions = httpContext.RequestServices.GetService<IOptions<JsonOptions>>()?.Value ?? new JsonOptions();
4752

@@ -66,6 +71,13 @@ private static void FormatSseItem(SseItem<T> item, IBufferWriter<byte> writer, J
6671
return;
6772
}
6873

74+
// Handle byte arrays byt writing them directly as strings.
75+
if (item.Data is byte[] byteArray)
76+
{
77+
writer.Write(byteArray);
78+
return;
79+
}
80+
6981
// For non-string types, use JSON serialization with options from DI
7082
var runtimeType = item.Data.GetType();
7183
var jsonTypeInfo = jsonOptions.SerializerOptions.GetTypeInfo(typeof(T));

src/Http/Http.Results/test/ServerSentEventsResultTests.cs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.IO.Pipelines;
45
using System.Net.ServerSentEvents;
56
using System.Reflection;
67
using System.Runtime.CompilerServices;
78
using System.Text;
89
using Microsoft.AspNetCore.Builder;
10+
using Microsoft.AspNetCore.Http.Features;
911
using Microsoft.AspNetCore.Http.Json;
1012
using Microsoft.AspNetCore.Http.Metadata;
1113
using Microsoft.AspNetCore.Routing;
@@ -31,6 +33,7 @@ public async Task ExecuteAsync_SetsContentTypeAndHeaders()
3133
Assert.Equal("text/event-stream", httpContext.Response.ContentType);
3234
Assert.Equal("no-cache,no-store", httpContext.Response.Headers.CacheControl);
3335
Assert.Equal("no-cache", httpContext.Response.Headers.Pragma);
36+
Assert.Equal("identity", httpContext.Response.Headers.ContentEncoding);
3437
}
3538

3639
[Fact]
@@ -259,6 +262,75 @@ async IAsyncEnumerable<string> GetEvents([EnumeratorCancellation] CancellationTo
259262
}
260263
}
261264

265+
[Fact]
266+
public async Task ExecuteAsync_DisablesBuffering()
267+
{
268+
// Arrange
269+
var httpContext = GetHttpContext();
270+
var events = AsyncEnumerable.Empty<string>();
271+
var result = TypedResults.ServerSentEvents(events);
272+
var bufferingDisabled = false;
273+
274+
var mockBufferingFeature = new MockHttpResponseBodyFeature(
275+
onDisableBuffering: () => bufferingDisabled = true);
276+
277+
httpContext.Features.Set<IHttpResponseBodyFeature>(mockBufferingFeature);
278+
279+
// Act
280+
await result.ExecuteAsync(httpContext);
281+
282+
// Assert
283+
Assert.True(bufferingDisabled);
284+
}
285+
286+
[Fact]
287+
public async Task ExecuteAsync_WithByteArrayData_WritesDataDirectly()
288+
{
289+
// Arrange
290+
var httpContext = GetHttpContext();
291+
var bytes = "event1"u8.ToArray();
292+
var events = new[] { new SseItem<byte[]>(bytes) }.ToAsyncEnumerable();
293+
var result = TypedResults.ServerSentEvents(events);
294+
295+
// Act
296+
await result.ExecuteAsync(httpContext);
297+
298+
// Assert
299+
var responseBody = Encoding.UTF8.GetString(((MemoryStream)httpContext.Response.Body).ToArray());
300+
Assert.Contains("data: event1\n\n", responseBody);
301+
302+
// Assert that string is not JSON serialized
303+
Assert.DoesNotContain("data: \"event1", responseBody);
304+
}
305+
306+
[Fact]
307+
public async Task ExecuteAsync_WithByteArrayData_HandlesNullData()
308+
{
309+
// Arrange
310+
var httpContext = GetHttpContext();
311+
var events = new[] { new SseItem<byte[]>(null) }.ToAsyncEnumerable();
312+
var result = TypedResults.ServerSentEvents(events);
313+
314+
// Act
315+
await result.ExecuteAsync(httpContext);
316+
317+
// Assert
318+
var responseBody = Encoding.UTF8.GetString(((MemoryStream)httpContext.Response.Body).ToArray());
319+
Assert.Contains("data: \n\n", responseBody);
320+
}
321+
322+
private class MockHttpResponseBodyFeature(Action onDisableBuffering) : IHttpResponseBodyFeature
323+
{
324+
public Stream Stream => new MemoryStream();
325+
public PipeWriter Writer => throw new NotImplementedException();
326+
public Task CompleteAsync() => throw new NotImplementedException();
327+
public void DisableBuffering() => onDisableBuffering();
328+
public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellationToken = default)
329+
=> throw new NotImplementedException();
330+
public Task StartAsync(CancellationToken cancellationToken = default)
331+
=> throw new NotImplementedException();
332+
}
333+
262334
private static void PopulateMetadata<TResult>(MethodInfo method, EndpointBuilder builder)
263335
where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(method, builder);
264336

0 commit comments

Comments
 (0)