Skip to content

Commit 9c83ed2

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

File tree

2 files changed

+85
-0
lines changed

2 files changed

+85
-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: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
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;
9+
using Castle.DynamicProxy.Generators.Emitters.SimpleAST;
810
using Microsoft.AspNetCore.Builder;
11+
using Microsoft.AspNetCore.Http.Features;
912
using Microsoft.AspNetCore.Http.Json;
1013
using Microsoft.AspNetCore.Http.Metadata;
1114
using Microsoft.AspNetCore.Routing;
@@ -31,6 +34,7 @@ public async Task ExecuteAsync_SetsContentTypeAndHeaders()
3134
Assert.Equal("text/event-stream", httpContext.Response.ContentType);
3235
Assert.Equal("no-cache,no-store", httpContext.Response.Headers.CacheControl);
3336
Assert.Equal("no-cache", httpContext.Response.Headers.Pragma);
37+
Assert.Equal("identity", httpContext.Response.Headers.ContentEncoding);
3438
}
3539

3640
[Fact]
@@ -259,6 +263,75 @@ async IAsyncEnumerable<string> GetEvents([EnumeratorCancellation] CancellationTo
259263
}
260264
}
261265

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

0 commit comments

Comments
 (0)