kevin.notes
← All field notesFROM THE NOTEBOOK

A Flux Does Not Tell the Browser How to Read It

Separating Reactor signals, HTTP framing, and browser behaviour clarified the difference between SSE and newline-delimited JSON.

KKevinLearning in public

My reactive programming notes use a movie notification example: create a movie, emit an event, and let connected clients receive it. It is a helpful exercise, but the word “stream” hides several separate decisions.

Reactor manages signals inside the application. HTTP carries bytes to a client. The client needs a format it understands. Returning a Flux addresses only part of that journey.

A movie event enters a Reactor sink, becomes a Flux, and is encoded either as SSE for EventSource or NDJSON for a streaming parser.

Start with what crosses the wire

An SSE response uses text/event-stream. An event can contain a name, an ID, and data, with a blank line ending the event. JSON is perfectly valid inside its data field:

id: movie-42
event: movie-created
data: {"id":"movie-42","title":"Example"}

Newline-delimited JSON uses a different shape: each line is a complete JSON value, without the SSE field prefixes or event separator:

{"id":"movie-42","title":"Example"}
{"id":"movie-43","title":"Another example"}

The payload can describe the same movie in both cases. That does not make the formats interchangeable.

Match the browser API to the response

The browser’s native EventSource understands SSE framing. For the named event above, a small client could look like this:

const events = new EventSource('/movies/events');
events.addEventListener('movie-created', event => {
  const movie = JSON.parse(event.data);
  console.log(movie.title);
});

This illustrative snippet is not a client I ran. Its point is the contract: EventSource parses the event, and the application parses the JSON inside data. It is not a general NDJSON reader. A fetch-based NDJSON client would need to buffer partial lines across network chunks before parsing them.

The HTML standard’s SSE section defines this event format and the browser’s reconnection behaviour. Reconnection is useful, but a new connection does not itself recover missed application events.

Keep the Reactor boundary clear

A sink gives application code a way to emit signals. Calling asFlux() exposes a publisher view for downstream subscribers; it does not turn the sink into a subscriber. Likewise, tryEmitNext() returns an outcome that the producer needs to inspect. A failed emission is not automatically retried successfully. These distinctions come from the Reactor Sinks guide.

Spring WebFlux then handles HTTP encoding. Its controller return-type documentation describes SSE responses using Flux<ServerSentEvent<?>>, or ordinary values with an appropriate text/event-stream response. The media type and encoding contract matter alongside the Java return type.

That separates two debugging questions: did the application emit the event, and did the client receive bytes in the format it expects?

Reconnect is not durable replay

SSE event IDs can let a reconnecting browser send a last-event identifier. The server still needs retained events and logic to resume from that identifier. An ID alone is not a history store.

A Reactor replay sink can retain an in-memory history for subscribers, with bounded options available. Keeping everything can consume unbounded memory, and process-local history does not survive a process restart. It also does not automatically implement SSE resumption from a browser’s last ID.

For the movie exercise, I would first decide whether clients need only live notifications or must recover every missed event. That requirement determines the storage and replay work. Choosing Flux is a useful implementation step, but it cannot answer the delivery question for me.

Adapted with AI assistance from my reactive streaming notes, correcting their SSE/NDJSON and sink terminology. The examples are explanatory; no Java endpoint or browser streaming test was executed for this article.

✳

Another question, a little clearer.

Back to the notebook ↗