Skip to content

Add support for ServerSentEventsResult and extension methods #60616

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Mar 3, 2025

Conversation

captainsafia
Copy link
Member

Implements #56172.

@Copilot Copilot AI review requested due to automatic review settings February 25, 2025 20:19
@captainsafia captainsafia added area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc and removed area-web-frameworks labels Feb 25, 2025
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

{
ArgumentNullException.ThrowIfNull(httpContext);

httpContext.Response.ContentType = "text/event-stream";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, and line 47 if that's something we care about.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bleh -- I am torn about adding the Firefox workaround here. That seems like something the SseFormatter should be doing? Although admittedly it's a little gross to hardcode workarounds for buggy clients.

Maybe we leave it out for now and see what kind of feedback we get?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, until we see feedback I think it's less interesting for an API. For SignalR it was important because we want to signal that the connection is active and let the user start sending messages.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason not to disable buffering?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems like something the SseFormatter should be doing?

What is that?

Copy link
Member

@davidfowl davidfowl Feb 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should do what SignalR is doing, we should not wait for feedback. Disable buffering and do the crazy IIS workaround 😄 (did you test this in IIS?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems like something the SseFormatter should be doing?

SignalR applies a special workaround to send some filler data through the SSE stream before the actual data to force Firefox's EventSource to emit an open event. It's a reaction to this bug in Firefox. The discussion is around whether or not to apply this workaround here in the ServerSentEventResult or have it be part of the default writing beahvior in the SseFormatter.

? jsonTypeInfo
: jsonOptions.SerializerOptions.GetTypeInfo(typeof(object));

var json = JsonSerializer.Serialize(item.Data, typeInfo);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There must be a more performant way to do this.
@eiriktsarpalis

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JsonSerializer.SerializeToUtf8Bytes?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slightly better since it avoids the string, but it still allocates a byte[] that we have to copy into IBufferWriter.

Copy link
Member

@lewing lewing Feb 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like SerializeToUtf8Bytes boils down to roughly

        using var jsonWriter = new Utf8JsonWriter(writer, jsonOptions.SerializerOptions);
        typeInfo.Write(jsonWriter, item.Data);

but I'm not familiar enough to be sure

Copy link
Member Author

@captainsafia captainsafia Feb 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like SerializeToUtf8Bytes/initializing a Utf8JsonWriter around the IBufferWriter<byte> might be as good as it gets here but I'd be curious to here what @eiriktsarpalis thinks.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to utilize ReusableUtf8JsonWriter?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would exposing JsonSerializer.Serialize accepting IBufferWriter<byte> overloads help in this case?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would exposing JsonSerializer.Serialize accepting IBufferWriter<byte> overloads help in this case?

Yep! I think that would be a nice solution.

{
ArgumentNullException.ThrowIfNull(httpContext);

httpContext.Response.ContentType = "text/event-stream";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason not to disable buffering?

/// Strings serialized by this result type are serialized as raw strings without any additional formatting.
/// </remarks>
#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters
public static IResult ServerSentEvents(IAsyncEnumerable<string> values, string? eventType = null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Event types are values scoped to individual events, and not the entire stream. Given that the SseItem<T> overload hardcodes to serialization, how can this API produce raw SSE events that includes other per-event fields such as event types?

I would guess that this was discussed during API review, but have you considered factoring into a separate method group (e.g. ServerSentEventsRaw) that also accepts IAsyncEnumerable<SseItem<string>>?

Copy link
Member

@eiriktsarpalis eiriktsarpalis Feb 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevermind me, it seems the comment in line 1004 is addressing my concern.

/// <param name="eventType">The event type to be included in the HTTP response body.</param>
/// <returns>The created <see cref="ServerSentEventsResult{T}"/> for the response.</returns>
/// <remarks>
/// Strings serialized by this result type are serialized as raw strings without any additional formatting.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to expose an option overriding this behavior?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We opted to expose fewer APIs for modifying the behavior of the formatter to start but I think we can pursue the proposal of adding an Action<Sse<T>, IBufferWriter> argument to these overloads in the event we hear feedback on it.


// If the event type is string, we can skip JSON serialization
// and directly use the SseFormatter's WriteAsync overload for strings.
if (_events is IAsyncEnumerable<SseItem<string>> stringEvents)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about byte[]?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's useful in scenaria where you need to write raw UTF-8 data, but it might make more sense if we exposed that using the IBufferWriter callback as with the underlying library.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but it might make more sense if we exposed that using the IBufferWriter callback as with the underlying library.

This seems like a good proposal. In the meantime, we can add special handling for byte arrays in our FormatSseItem callback.

@ANahr
Copy link

ANahr commented Feb 27, 2025

The implementation doesn't seem to have any keep-alive mechanism.
In real live web applications it is likely that users will run into a lot of problems with proxies, firewalls and all kind of other systems that cut the connection between server and client after a certain time without any data sent.
If you take the "Basic usage with simple values" example and set the delay to 100000ms you will likely never get a second event when running the code outside a private network.

For a general purpose usable implementation it should send a kind of keep-alive message if no user-provided message is sent within a specific timeframe (e.g. 5/10/20/30/tbd seconds).

@captainsafia
Copy link
Member Author

@ANahr Thanks for providing this feedback! I err on the side of keeping the API simpler here. It's possible for users to compose their own keep-alive message on a period timer on top of this API. The one gotcha is that there's no way to transmit comment messages (not complying with the SSE event format) with the current use of the SseFormatter.

@captainsafia captainsafia merged commit bd1ba76 into main Mar 3, 2025
27 checks passed
@captainsafia captainsafia deleted the cs/minimal-sse-result branch March 3, 2025 17:20
@dotnet-policy-service dotnet-policy-service bot added this to the 10.0-preview3 milestone Mar 3, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants