Server-Sent Events
Server-Sent Events (SSE) provide a simple, one-directional channel for pushing updates from the server to the browser over a standard HTTP connection. Unlike WebSockets, SSE uses plain HTTP, works through most proxies and firewalls, and supports automatic reconnection out of the box. Use SSE when you need server-to-client streaming without bidirectional communication.
Basic setup
Section titled “Basic setup”-
Add the SSE middleware to a scoped route
const App = pidgn.Router.define(.{.middleware = &.{pidgn.errorHandler(.{}),pidgn.logger,},.routes = &.{pidgn.Router.get("/", index),pidgn.Router.scope("/events", .{.middleware = &.{pidgn.sseMiddleware(.{})},}, &.{pidgn.Router.get("", sseHandler),}),},}); -
Write the SSE handler
fn sseHandler(ctx: *pidgn.Context) !void {if (ctx.getAssign("sse")) |_| {// Client accepts text/event-streamctx.respond(.ok, "text/event-stream", "event: message\ndata: Hello from SSE!\n\n");return;}ctx.text(.bad_request, "This endpoint requires an SSE connection.");}
SseConfig
Section titled “SseConfig”| Option | Type | Default | Description |
|---|---|---|---|
extra_headers | []const [2][]const u8 | &.{} | Additional response headers as [name, value] pairs. |
pidgn.sseMiddleware(.{ .extra_headers = &.{ .{ "X-Accel-Buffering", "no" }, // Disable Nginx buffering },}),How the middleware works
Section titled “How the middleware works”When a request arrives at a route guarded by sseMiddleware:
- The middleware checks the
Acceptheader fortext/event-stream. - If present, it sets these response headers:
Content-Type: text/event-streamCache-Control: no-cacheConnection: keep-alive- Plus any
extra_headersfrom the config.
- It assigns
"sse" = "true"to the context. - The downstream handler runs and can check
ctx.getAssign("sse")to confirm the SSE connection.
If the client does not send Accept: text/event-stream, the middleware still calls the downstream handler, but without setting headers or the assign. This lets you serve both SSE and regular responses from the same route if needed.
SSE event format
Section titled “SSE event format”SSE follows a simple text protocol. Each event is a block of field: value lines separated by a blank line:
event: messagedata: Hello, world!
event: updatedata: {"count": 42}id: 1The standard fields are:
| Field | Description |
|---|---|
data | The event payload. Multiple data lines are joined with newlines. |
event | The event type name. The client listens with addEventListener(type, ...). |
id | Sets the last event ID for reconnection. |
retry | Suggests a reconnection interval in milliseconds. |
SseWriter
Section titled “SseWriter”For persistent SSE connections where you need to send multiple events over time, use the SseWriter:
fn streamHandler(ctx: *pidgn.Context) !void { var writer = pidgn.SseWriter.init(ctx);
try writer.sendEvent("Hello from SSE!"); try writer.sendNamedEvent("update", "{\"count\": 1}"); try writer.sendWithId("update", "{\"count\": 2}", "evt-2"); try writer.keepAlive(); // sends a comment line to prevent timeout}SseWriter API
Section titled “SseWriter API”| Method | Description |
|---|---|
sendEvent(data) | Send an unnamed event with data. |
sendNamedEvent(event, data) | Send a named event with event type and data. |
sendWithId(event, data, id) | Send a named event with an id field for reconnection tracking. |
keepAlive() | Send a comment line (: keepalive) to prevent proxy/client timeouts. |
Client-side usage
Section titled “Client-side usage”Browsers natively support SSE via the EventSource API:
<script>const source = new EventSource("/events");
// Listen for unnamed eventssource.onmessage = (e) => { console.log("message:", e.data);};
// Listen for named eventssource.addEventListener("update", (e) => { const data = JSON.parse(e.data); console.log("update:", data);});
// Handle errors and reconnectionsource.onerror = (e) => { console.log("SSE error, will auto-reconnect");};</script>EventSource automatically reconnects if the connection drops, using the last event id to resume where it left off.
Scoped route example
Section titled “Scoped route example”A common pattern is to scope SSE routes under a prefix:
pidgn.Router.scope("/api/events", .{ .middleware = &.{ pidgn.bearerAuth(.{ .validate = &myValidator }), pidgn.sseMiddleware(.{ .extra_headers = &.{ .{ "X-Accel-Buffering", "no" }, }, }), },}, &.{ pidgn.Router.get("/notifications", notificationStream), pidgn.Router.get("/activity", activityStream),}),Next steps
Section titled “Next steps”- WebSockets — bidirectional communication when you need client-to-server messages
- Middleware — understand the middleware pipeline and scoping
- Context — response helpers and assigns
- Controllers — organize your handlers
- Response Cache — cache GET responses (not applicable to SSE, but useful for other endpoints)