Skip to main content

SSE

Juneau supports the Server-Sent Events (SSE) wire format (text/event-stream) via native serializer and parser implementations under org.apache.juneau.marshall.sse. SSE is the one-way push channel used by browsers (EventSource) and most LLM streaming APIs (OpenAI, Anthropic, etc.) for progress events, partial responses, heartbeats, and other long-lived telemetry feeds.

The SseSerializer emits one wire event per element with a Writer.flush() after each one — that flush is what makes SSE actually push events to the client immediately rather than buffer them until the end of the response. The SseParser provides an eager parse path that drains a closed stream into a List<SseEvent>. For long-lived streams that outlive a normal parser-session lifecycle, use the SseEventReader iterator directly.

SSE support is included in juneau-marshall.

Maven
<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-marshall</artifactId>
<version>${juneau.version}</version>
</dependency>

The SseEvent bean​

SseEvent carries the four spec-defined fields:

FieldTypePurpose
eventStringEvent-type name. When absent, the WHATWG EventSource dispatches as "message".
dataStringThe payload, pre-serialized. Embedded \n causes the wire to emit multiple data: lines.
idStringLast-event-id, used for resumability.
retryLongReconnect-delay hint in milliseconds.

The data field is intentionally a pre-serialized string — Juneau's SSE marshaller does not compose with a delegate marshaller. Callers serialize their typed payload through whichever marshaller they prefer and assign the resulting string:

SseEvent event = new SseEvent()
.setEvent("progress")
.setData(Json.of(Map.of("step", 4, "total", 10)))
.setId("42")
.setRetry(5000L);

Quick Start — Serializing​

List<SseEvent> events = List.of(
new SseEvent("progress", "step 1").setId("1"),
new SseEvent("progress", "step 2").setId("2"),
new SseEvent("progress", "step 3").setId("3")
);

String wire = Sse.of(events);

Produces:

event: progress
data: step 1
id: 1

event: progress
data: step 2
id: 2

event: progress
data: step 3
id: 3

Quick Start — Parsing​

// Eager parse into a List<SseEvent> (good for one-shot reads and tests).
List<SseEvent> parsed = Sse.to(wire, List.class, SseEvent.class);

// Single-event target — returns the first event in the stream.
SseEvent first = Sse.to(wire, SseEvent.class);

// Array target.
SseEvent[] arr = Sse.to(wire, SseEvent[].class);

For long-lived streams (the typical SSE use case), drive the parser iterator directly so the underlying Reader stays open until you close it:

try (SseEventReader r = new SseEventReader(responseReader)) {
while (r.hasNext()) {
SseEvent e = r.next();
// dispatch...
}
}

REST Integration​

SSE is not included in BasicUniversalConfig — it's a streaming push format with very different semantics from the standard content-negotiation marshallers (long-lived response, no content length, per-event flush). Register it explicitly on the resource that needs it:

@Rest(serializers = SseSerializer.class)
public class MyStreamResource {

@RestGet("/events")
public List<SseEvent> stream() {
return List.of(
new SseEvent("progress", "step 1"),
new SseEvent("progress", "step 2")
);
}
}

Per-event flush works through the standard SerializedPojoProcessor path — SseSerializerSession calls Writer.flush() after each event so the servlet container pushes the wire bytes immediately without waiting for the response to complete.

Comment / heartbeat lines​

Many HTTP intermediaries (proxies, load balancers) close idle connections after some timeout. Use SseSerializer.writeComment(Writer, String) to emit an SSE comment line every 15–30 s on long-lived streams to keep the socket alive:

SseSerializer.writeComment(responseWriter, "ping");

Produces:

: ping

WHATWG Spec Compliance​

The parser implements the full WHATWG parse algorithm:

  • All three line terminators: CR, LF, CR LF.
  • One-time UTF-8 BOM strip on the first character of the stream.
  • Optional single space stripped after the field colon (data: hello and data:hello parse the same).
  • Multi-line data: accumulation, joined with \n on dispatch.
  • Comment lines (lines starting with :) are skipped.
  • Lines with no colon treated as a field name with empty value.
  • id values containing U+0000 NULL are ignored.
  • retry values that aren't ASCII-digit integers are ignored.

Media Types​

  • Produces: text/event-stream
  • Accepts: text/event-stream
See Also