# Server-Sent Events _Path: en/http/sse_ ## Table of Contents - Server-Sent Events ## Content # Server-Sent Events The SSE middleware streams events from the server to HTTP clients using the [Server-Sent Events](https://html.spec.whatwg.org/multipage/server-sent-events.html) protocol. Two mechanisms are available: **direct streaming** from an HTTP handler, and **process-backed relay** via the `sse_relay` middleware. ## Direct Streaming Use `res:write_event()` to send SSE events directly from an HTTP handler. The response automatically switches to SSE mode on the first call, setting appropriate headers. ```lua local http = require("http") local function handler() local res = http.response() res:write_event({name = "status", data = {state = "started"}}) res:write_event({name = "progress", data = {percent = 50}}) res:write_event({name = "status", data = {state = "complete"}}) end ``` Each event requires a `name` and `data` field. The `data` value is JSON-encoded automatically. Direct streaming is suitable for short-lived request-response flows like progress updates. For long-lived connections managed by background processes, use the SSE Relay. ## SSE Relay The SSE Relay middleware creates long-lived SSE streams backed by processes. It follows the same relay pattern as [WebSocket Relay](http/websocket-relay.md). ### How It Works 1. HTTP handler sets `X-SSE-Relay` header with a JSON relay configuration 2. Middleware intercepts the response and creates an SSE session 3. Session registers as a process with its own PID 4. Messages sent to the session PID are forwarded as SSE events to the client ## Process Semantics SSE streams are full processes with their own PID. They integrate with the process system: - **Addressable** — Any process can send messages to a stream PID - **Monitorable** — Processes can monitor SSE streams for exit events - **Linkable** — SSE streams can be linked to other processes - **EXIT events** — When a stream closes, monitors receive exit notifications ```lua -- Send event to SSE client from any process process.send(stream_pid, "sse.message", {event = "update", value = 42}) -- Monitor an SSE stream process.monitor(stream_pid) ``` The relay monitors the target process. If the target exits, the SSE stream closes automatically and the client receives a `done` event. ## Configuration Add as post-match middleware on a router: ```yaml - name: sse_router kind: http.router meta: server: gateway prefix: /sse post_middleware: - sse_relay post_options: sserelay.allowed.origins: "https://app.example.com" ``` | Option | Description | |--------|-------------| | `sserelay.allowed.origins` | Comma-separated allowed origins (supports wildcards) | If no origins are configured, only same-origin requests are allowed. ## Handler Setup The HTTP handler spawns a process and configures the relay: ```lua local http = require("http") local json = require("json") local function handler() local res = http.response() -- Spawn handler process local pid = process.spawn("app.sse:handler", "app:processes") -- Configure relay res:set_header("X-SSE-Relay", json.encode({ target_pid = tostring(pid), message_topic = "sse.message", heartbeat_interval = "30s", metadata = { user_id = http.request():query("user_id") } })) end ``` ### Relay Config Fields | Field | Type | Default | Description | |-------|------|---------|-------------| | `target_pid` | string | — | Process PID to receive messages (omit for detached mode) | | `message_topic` | string | `sse.message` | Topic filter for forwarded events | | `heartbeat_interval` | duration | `30s` | Heartbeat frequency (e.g. `30s`, `1m`) | | `idle_timeout` | duration | — | Close stream after inactivity | | `hard_timeout` | duration | — | Close stream after absolute duration | | `metadata` | object | — | Attached to join/leave/heartbeat messages | ### Managed Mode When `target_pid` is set, the relay operates in managed mode: - Monitors the target process - Sends `sse.join` on connect and `sse.leave` on disconnect - Closes the stream automatically if the target exits ### Detached Mode When `target_pid` is omitted, the relay starts in detached mode: - Emits a `ready` event to the client with `stream_pid` and `message_topic` - No process is monitored initially - A process can attach later by sending an `sse.control` message ```lua -- Detached setup: no target_pid res:set_header("X-SSE-Relay", json.encode({ heartbeat_interval = "30s" })) ``` The client receives a `ready` event: ```json {"stream_pid": "sse@node/abc123", "message_topic": "sse.message"} ``` ## Message Topics The relay uses these topics for communication between the stream and target process: | Topic | Direction | When | Payload | |-------|-----------|------|---------| | `sse.join` | stream → target | Client connects | `client_pid`, `metadata` | | `sse.message` | target → stream | Default event topic | Forwarded as SSE event | | `sse.heartbeat` | stream → target | Periodic (if configured) | `client_pid`, `uptime`, `message_count` | | `sse.leave` | stream → target | Client disconnects | `client_pid`, `metadata` | | `sse.control` | any → stream | Control command | Relay config fields | | `sse.close` | any → stream | Force close | Optional reason string | ## Receiving in Target Process ```lua local json = require("json") local function handler() local inbox = process.inbox() while true do local msg, ok = inbox:receive() if not ok then break end local topic = msg:topic() local data = msg:payload():data() if topic == "sse.join" then local client_pid = data.client_pid elseif topic == "sse.heartbeat" then -- Periodic health check elseif topic == "sse.leave" then cleanup(data.client_pid) end end end ``` ## Sending Events Send events to the client by messaging the stream PID: ```lua -- Send on the default message topic process.send(stream_pid, "sse.message", { event = "update", value = 42 }) -- Force close the stream process.send(stream_pid, "sse.close", "session expired") ``` Events sent on the configured `message_topic` are forwarded to the client as SSE events. The topic name becomes the SSE event name. ## Connection Transfer Send a control message to change the target process, topic filter, or timeouts dynamically: ```lua process.send(stream_pid, "sse.control", { target_pid = tostring(new_pid), message_topic = "custom.topic", idle_timeout = "5m" }) ``` When the target changes, the relay sends `sse.leave` to the old target and `sse.join` to the new one. Set `target_pid` to an empty string to detach without reattaching. ## See Also - [Middleware](http/middleware.md) — Middleware configuration - [WebSocket Relay](http/websocket-relay.md) — WebSocket equivalent - [Process](lua/core/process.md) — Process messaging ## Navigation Previous: WebSocket Relay (http/websocket-relay) Next: Hello World (tutorials/hello-world)