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 | |
|---|
The SseEvent bean
SseEvent carries the four spec-defined fields:
| Field | Type | Purpose |
|---|---|---|
event | String | Event-type name. When absent, the WHATWG EventSource dispatches as "message". |
data | String | The payload, pre-serialized. Embedded \n causes the wire to emit multiple data: lines. |
id | String | Last-event-id, used for resumability. |
retry | Long | Reconnect-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: helloanddata:helloparse the same). - Multi-line
data:accumulation, joined with\non dispatch. - Comment lines (lines starting with
:) are skipped. - Lines with no colon treated as a field name with empty value.
idvalues containing U+0000 NULL are ignored.retryvalues that aren't ASCII-digit integers are ignored.
Media Types
- Produces:
text/event-stream - Accepts:
text/event-stream
- WHATWG HTML §9.2 — Server-sent events for the full wire-format spec.
- Marshallers for the marshaller API overview.
- Token-Streaming Basics — SSE is a
PARTIALstreaming format (record cursor only); an SSE event isn't a clean structural token sequence. - Server-Sent Events in REST servers