SSE (Server-Sent Events)¶
yorishiro-proxy detects and handles Server-Sent Events (SSE) responses at the event level. The SSE Layer (internal/layer/sse/) wraps the response side of an HTTP/1.x exchange whose body is a text/event-stream and re-shapes the byte stream into one SSEMessage envelope per parsed event.
L7 structured view: yes. L4 raw bytes: yes. Per-event envelopes flow through a streaming-aware Pipeline.
How detection works¶
SSE detection happens at the HTTP response level. When the upstream server returns a response with Content-Type: text/event-stream, the connector swaps the response Channel into the SSE Layer. The MIME type check ignores parameters like charset, so text/event-stream; charset=utf-8 is also detected. The SSE Layer itself contains no detection logic — it is a Channel adapter.
SSE is not a separate transport — it rides on top of HTTP/1.x (or HTTPS via MITM). The request phase flows through the HTTP/1.x Layer normally, and the response side switches to event-level streaming.
Client ──HTTP Request──> yorishiro-proxy ──HTTP Request──> Upstream Server
<──SSE Events─── <──SSE Events───
Event parsing¶
The proxy parses SSE events according to the HTML Living Standard. Each event is delimited by a blank line and can contain the following fields:
| Field | Description |
|---|---|
event: |
Event type name (default is "message" if omitted) |
data: |
Event payload (multiple data: lines are joined with newlines) |
id: |
Last event ID for reconnection |
retry: |
Reconnection interval hint (milliseconds) |
Lines starting with : are treated as comments and silently consumed. Unknown fields are ignored per the specification.
Size limits¶
- Single event: capped at 1 MB of raw bytes to prevent memory exhaustion
- Events per stream: up to 10,000 events are recorded per stream; beyond this limit, events are still forwarded to the client but no longer saved to the flow store
- Event body recording: event data is truncated at 254 MB per event in the flow store
- Stream duration: a safety cap of 24 hours prevents indefinite resource consumption
Event-level recording¶
Each parsed SSE event is emitted as a separate envelope with Direction=Receive and Envelope.Protocol=sse. The first call to Next returns a clone of the original response Envelope with Protocol overridden to ProtocolSSE (preserving Sequence). Subsequent Next calls drive the SSE parser over the body reader and emit one envelope per parsed event with Message=*envelope.SSEMessage and Envelope.Raw containing the wire bytes of the event (including the trailing blank line).
Send is a programmer error on the SSE Layer; it returns the sentinel ErrSendUnsupported. SSE is half-duplex server→client.
Flow structure¶
An SSE flow is recorded with:
- Protocol:
sse - Flow type:
stream - State:
activewhile the stream is open,completewhen it ends
Event message metadata¶
Each event message includes the following metadata fields:
| Key | Value |
|---|---|
sse_type |
"event" for SSE events, "headers" for the initial response headers |
sse_event |
Event type from the event: field (omitted if empty) |
sse_id |
Event ID from the id: field (omitted if empty) |
sse_retry |
Retry value from the retry: field (omitted if empty) |
The event Data is stored on the SSEMessage Message struct, and the original wire-format bytes (including the trailing blank line) are preserved on Envelope.Raw.
Intercept support¶
SSE supports intercept at two levels:
Response-level intercept¶
When the SSE response matches intercept rules, the entire response is held for review before any events are streamed. At this level:
- Release: the stream proceeds normally
- Drop: the stream is terminated and the client receives a 502 error
- Modify: not supported at the response level (the body is a stream); treated as release with a warning
Event-level intercept¶
Individual SSE events can be intercepted and held for review. All three actions are supported:
- Release: the event is forwarded unchanged
- Drop: the event is silently skipped (not forwarded to the client)
- Modify: the event data and metadata can be modified before forwarding
Event metadata is exposed as pseudo-headers for intercept rule matching:
| Header | Maps to |
|---|---|
X-SSE-Event |
Event type (event: field) |
X-SSE-Id |
Event ID (id: field) |
X-SSE-Retry |
Retry value (retry: field) |
Variant tracking¶
When an event is modified by intercept or a plugin, both the original and modified versions are recorded:
- Sequence N: original event (metadata
variant=original) - Sequence N+1: modified event (metadata
variant=modified)
Unmodified events are recorded as a single message without variant metadata.
Plugin hooks¶
The SSE Layer participates in the standard Pipeline. Plugin authors register hooks via the pluginv2 engine using the (protocol, event, phase) triple — for example (sse, on_event, pre) to observe each event envelope before intercept. See the Plugin hook reference for the full surface.
Safety filter¶
The output filter (PII masking) is applied per-event to the SSE Data field before forwarding to the client:
- Mask rules: matching data is replaced with masked values in the forwarded event; the flow store always records the original (unfiltered) data
- Block rules: if a block-action rule matches an event's data, the entire SSE stream is terminated immediately
When events are displayed in the intercept queue, the output filter is also applied so that the reviewing agent sees masked data.
Auto-transform¶
Response auto-transform rules are not applied to SSE streams. Auto-transform requires buffering the full response body in memory, which is not possible for a streaming response.
Query examples¶
Find SSE flows¶
The canonical Envelope.Protocol value for SSE is sse:
Get SSE flow details¶
The response includes flow_type: "stream", message_count with the total number of recorded events, and message_preview with the first 10 messages.
Page through SSE events¶
Filter receive-only messages¶
// query
{
"resource": "messages",
"id": "flow-abc-123",
"filter": {"direction": "receive"},
"limit": 50
}
Processing pipeline¶
Each SSE event passes through the following steps in order:
- Parse --
SSEParser.Next()reads and parses the next event from the stream - Plugin hook --
on_receive_from_server(event-level) - Snapshot -- original event data is captured for variant tracking
- Intercept -- event is checked against intercept rules; if matched, held for review (release / drop / modify)
- Plugin hook --
on_before_send_to_client(event-level) - Record -- event is saved to the flow store (with variant if modified)
- Output filter -- PII masking / block rules applied to the data
- Forward -- the (possibly filtered) event bytes are written to the client
TCP-forward dispatch¶
SSE is also a valid operator-declared protocol value on a tcp_forwards entry (USK-913). Setting protocol: "sse" on a forward declares that the upstream returns a streaming text/event-stream response, mirroring "websocket" as the declared expectation that the request initiates an RFC 6455 Upgrade. The dispatcher then routes the connection through the SSE Layer the same way a CONNECT-MITM stream would be. See proxy_start TCP forwards.
Intercept hold-timeout¶
SSE inherits the per-protocol intercept hold-timeout override system (USK-855). The built-in default for sse is 60 s (raised from the 8 s global default in USK-855 / #860), giving operators a realistic review window before the upstream's idle timer fires. Configure overrides via configure.intercept_queue.protocol_overrides.
Limitations¶
- Receive-only Layer -- SSE is half-duplex server→client;
Sendon the SSE Channel returnsErrSendUnsupported - No auto-transform -- response auto-transform rules require full body buffering and are skipped for SSE
- Response-level modify --
modify_and_forwardis not supported at the response header level for SSE (treated as release)
Related pages¶
- HTTP/1.x -- the HTTP transport that SSE rides on
- HTTPS MITM -- SSE over HTTPS connections
- Intercept -- intercept rules and the review queue
- SafetyFilter -- output filter rules for PII masking
- Plugin hook reference -- detailed hook documentation