Skip to content

[DO NOT MERGE] WIP, add/backport generic server-sent events support to Azure Core - #50081

Draft
Xiaofei Cao (XiaofeiCao) wants to merge 28 commits into
Azure:mainfrom
XiaofeiCao:sse-generic-listener-squashed
Draft

[DO NOT MERGE] WIP, add/backport generic server-sent events support to Azure Core#50081
Xiaofei Cao (XiaofeiCao) wants to merge 28 commits into
Azure:mainfrom
XiaofeiCao:sse-generic-listener-squashed

Conversation

@XiaofeiCao

@XiaofeiCao Xiaofei Cao (XiaofeiCao) commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Scope

Adds Azure Core support for single-response generic SSE consumption. It parses one established Response<BinaryData>; it never reconnects, replays, or sends Last-Event-Id.

  • New: ServerSentEvent<T>, ServerSentEventListener<T>, ServerSentEventStreams, incremental SSE parsing, body-owned response lifecycle, and optional typed terminal-event predicates.
  • Updated: REST proxy live-body preservation, response logging protection, SSE media-type helpers, and non-replayable BinaryData streaming.
  • Unchanged: HTTP pipeline, general policies, transports, and Reactor.
  • Separate stacked work: generated client APIs, shared proxy declarations, and generated event conversion.

Contract

ServerSentEventStreams is exported from com.azure.core.http; parser and response-validation types remain internal. REST proxy methods return an ordinary, non-Closeable Response<BinaryData>. Its non-replayable body owns the physical HttpResponse and releases it when body consumption completes, fails, is interrupted, or is cancelled. Validation failures cancel an available body before propagating the error.

Non-204 responses require text/event-stream content. Event streams are decoded as UTF-8 regardless of an optional declared charset. Existing overloads complete on EOF or HTTP 204. Predicate overloads deliver the matching typed terminal event and complete; EOF before a match is an incomplete-stream error.

Accept: text/event-stream preserves a live body only when its q value is syntactically valid and greater than zero.

Validation

  • Focused SSE, media-type, REST proxy, FluxInputStream, and BinaryData tests
  • Full Azure Core unit and integration test suite
  • Azure Core package build, Checkstyle, SpotBugs, JavaDoc, and Revapi

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: ff0a367c-664e-4de0-902b-ddaae2c86546
@github-actions github-actions Bot added the Azure.Core azure-core label Aug 10, 2026
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
34 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@XiaofeiCao Xiaofei Cao (XiaofeiCao) changed the title Add/backport generic server-sent events support to Azure Core WIP, add/backport generic server-sent events support to Azure Core Aug 11, 2026
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: ff0a367c-664e-4de0-902b-ddaae2c86546

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The incremental parser needs a bounded pending-event size before this is safe to ship.

appendByte doubles lineBytes without a maximum, and completed data: lines are accumulated in data until a blank line. A server (or intermediary) can therefore send either one unterminated line or an unlimited sequence of data: lines without an empty-line dispatch and make the client retain the entire stream until heap exhaustion. This affects both the async and listener paths.

Please enforce a documented maximum line/event size and terminate the stream with a clear exception when it is exceeded. Add tests for both an oversized single line and many bounded lines without a terminating blank line.

@XiaofeiCao

Copy link
Copy Markdown
Contributor Author

The incremental parser needs a bounded pending-event size before this is safe to ship.

appendByte doubles lineBytes without a maximum, and completed data: lines are accumulated in data until a blank line. A server (or intermediary) can therefore send either one unterminated line or an unlimited sequence of data: lines without an empty-line dispatch and make the client retain the entire stream until heap exhaustion. This affects both the async and listener paths.

Please enforce a documented maximum line/event size and terminate the stream with a clear exception when it is exceeded. Add tests for both an oversized single line and many bounded lines without a terminating blank line.

Some thoughts(evidence collected by agent):

Spring's 256kb limit doesn't seem to fit our knowledgebase retrieval case. Since currently we don't have configuration for response body size(correct me if I'm wrong), I'd say we don't enforce this.

@weidongxu-microsoft

Copy link
Copy Markdown
Member

Three minor follow-ups from the API/protocol pass:

  1. ServerSentEventStreams publicly accepts any Response<BinaryData>, but internally requires the runtime response to implement Closeable. Consider making that ownership requirement explicit in the API/type contract, or at least documenting it, so wrapped/custom responses don't fail unexpectedly.
  2. isTextEventStreamContentType currently accepts an explicitly non-UTF-8 charset such as text/event-stream; charset=iso-8859-1, while the parser always decodes UTF-8. SSE is UTF-8-only, so an incompatible charset declaration should be rejected.
  3. acceptsTextEventStream treats malformed or out-of-range quality values such as q=bogus, q=-1, and q=2 as enabled. It would be safer to validate the HTTP q-value grammar and only enable the media range for a valid value greater than zero.

These are non-blocking compared with the pending-event size concern.

@weidongxu-microsoft

Copy link
Copy Markdown
Member

Agent-generated Java SSE interface design comparison and recommendations: https://gist.github.com/weidongxu-microsoft/f99f792a78f16cef8c9c1674c68e7d1b

@weidongxu-microsoft

Weidong Xu (weidongxu-microsoft) commented Aug 12, 2026

Copy link
Copy Markdown
Member

Agent-generated Java SSE interface design comparison and recommendations: https://gist.github.com/weidongxu-microsoft/f99f792a78f16cef8c9c1674c68e7d1b

Summary aside, this is more or less the "alternative design" we've discussed. A closable stream pattern for sync.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: daa3657a-91d5-48fa-b7ae-6983091cab6a
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: daa3657a-91d5-48fa-b7ae-6983091cab6a
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: daa3657a-91d5-48fa-b7ae-6983091cab6a
* Parsing an event stream</a>
*/
@Immutable
public final class ServerSentEvent<T> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd prefer we kept closer to the protocol layer with the typing here and remove <T>. Replacing T data with String data and letting the SSE consumer handle String -> T.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There's ServerSentEventFrame for this: https://github.com/XiaofeiCao/azure-sdk-for-java/blob/5fa41d4a0f0c64386c5930c5c676a88a6039e32d/sdk/core/azure-core/src/main/java/com/azure/core/implementation/util/ServerSentEventStream.java#L469

In this draft, I'm exposing ServerSentEvent to user via

Flux<ServerSentEvent<KnowledgeBaseRetrievalStreamEvent>> retrieveStream();

, maybe a generic one would be more suitable?

});
}

private ServerSentEvent(String id, String event, T data, String comment, Duration retryAfter) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's not hide this constructor, it really doesn't benefit us to do so. Let's scope the constructor to properties that are required to create an event and add setters and getters for optional properties. Or, just keep everything in the constructor and pass null accordingly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If I'm understanding correctly, ServerSentEvent is kinda like an immutable response model.
If it's exposed to user, I'd prefer hiding it. But we could have more discussion on whether to expose it, or directly return, e.g. Flux<KnowledegBaseRetrievalStreamEvent. Would like to hear your opinion on this one.

return true;
}

private static boolean hasPositiveQuality(List<String> mediaRange) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we have any cases where Azure services use multiple Accept header values and use this part of the HTTP spec?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines +128 to +130
if (equalsIndex > 0
&& "charset".equalsIgnoreCase(parameter.substring(0, equalsIndex).trim())
&& !isUtf8(parameter.substring(equalsIndex + 1).trim())) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why do we fail if the charset isn't UTF-8? We are capable of supporting other character encodings, even have CoreUtils.bomAwareToString which returns a String based on a leading byte order mark or the charset defined in the Content-Type header.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

good question, it is for simplicity here.. yeah, we probably don't want to restrict the encoding here.

CoreUtils.bomAwareToString seems to require reading the whole stream, which may not be suitable for sse stream here, CMIIW.

I'm letting agent figure this out..

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done by agent. Hopefully the correctness is ensured by the added tests..

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Seems sse can only be decoded as utf8:

Event streams are always decoded as UTF-8. There is no way to specify another character encoding.

See https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events-intro

private static <T> Flux<ServerSentEvent<T>> toFlux(Response<BinaryData> response,
BiFunction<String, String, T> deserializer, TerminalEventPolicy<T> terminalPolicy) {
AtomicBoolean subscribed = new AtomicBoolean();
return Flux.defer(() -> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we instead write a Subscriber / Flux type for this, similar to what we did for FluxUtil.writeFile and FluxUtil.readFile?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

agent produced additional 250+ lines of code and pretty hard to reason about.

Compared to FluxUtil.writeFile, there's extra complexity which standard Reactor operator already handles(below is from agent):

For SSE it had to additionally handle parser state across buffers, zero-to-many events per buffer, queued frames, downstream demand, EOF flushing, synchronous errors, cancellation, and reentrant requests. It duplicated Reactor’s demand, cancellation, and signal-ordering machinery. Standard Reactor operators express those guarantees more clearly, so I simplified it to a per-subscription decoder pipeline with bounded read-ahead.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: daa3657a-91d5-48fa-b7ae-6983091cab6a
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: daa3657a-91d5-48fa-b7ae-6983091cab6a
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: daa3657a-91d5-48fa-b7ae-6983091cab6a
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: daa3657a-91d5-48fa-b7ae-6983091cab6a
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: daa3657a-91d5-48fa-b7ae-6983091cab6a
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: daa3657a-91d5-48fa-b7ae-6983091cab6a
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: daa3657a-91d5-48fa-b7ae-6983091cab6a
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: daa3657a-91d5-48fa-b7ae-6983091cab6a
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: daa3657a-91d5-48fa-b7ae-6983091cab6a
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: daa3657a-91d5-48fa-b7ae-6983091cab6a
@XiaofeiCao Xiaofei Cao (XiaofeiCao) changed the title WIP, add/backport generic server-sent events support to Azure Core [DO NOT MERGE] WIP, add/backport generic server-sent events support to Azure Core Aug 19, 2026
Xiaofei Cao (XiaofeiCao) and others added 13 commits August 19, 2026 17:31
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: daa3657a-91d5-48fa-b7ae-6983091cab6a
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: daa3657a-91d5-48fa-b7ae-6983091cab6a
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: daa3657a-91d5-48fa-b7ae-6983091cab6a
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: c6a36a59-cfcf-4a11-b7d3-967de8071bd3
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: c6a36a59-cfcf-4a11-b7d3-967de8071bd3
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 607c904a-42fc-440a-b53d-dbb61a5cbd4e
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 607c904a-42fc-440a-b53d-dbb61a5cbd4e
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 607c904a-42fc-440a-b53d-dbb61a5cbd4e
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 48a66eea-bdde-4a8f-95c6-a745a6bf01c0
Replace the custom Flux and Subscriber state machine with standard Reactor operators while preserving bounded read-ahead and response-body lifecycle behavior.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: c769c73d-fd59-442e-b297-7f98b28db34b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 50f0c93b-2784-4012-9f6d-2f666e82a4e0
Keep terminal predicates as inclusive early-stop signals without treating EOF or HTTP 204 as an error.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 50f0c93b-2784-4012-9f6d-2f666e82a4e0
Remove the private terminal-event policy wrapper and pass the optional predicate directly through internal stream processing.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 50f0c93b-2784-4012-9f6d-2f666e82a4e0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Azure.Core azure-core

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants