Controllers

Server-Sent Events

Stream long-lived Server-Sent Events from controller actions.

By Guillermo Alvarez - Published Updated

Start a stream

Use SSEStream from a controller action:

func (c *NotificationsController) Events() error {
    stream, err := c.SSEStream()
    if err != nil {
        return err
    }
    defer stream.Close()

    return stream.Send(lazysse.Event{
        Event: "ready",
        Data:  []string{"connected"},
    })
}

Starting the stream commits the response headers and skips automatic view rendering.

Send events

lazysse.Event maps directly to SSE fields:

err := stream.Send(lazysse.Event{
    Event:   "message",
    ID:      "123",
    Data:    []string{"first line", "second line"},
    Comment: []string{"optional comment"},
    Retry:   5 * time.Second,
})

Each Data entry becomes a data: line. Entries containing newlines are split into multiple data lines.

Keep the stream alive

Heartbeats keep intermediaries from closing idle connections:

stream.Heartbeat(10 * time.Second)

Long-running actions should exit when the client disconnects:

for {
    select {
    case <-stream.Done():
        return nil
    case event := <-events:
        if err := stream.Send(event.SSEEvent()); err != nil {
            return err
        }
    }
}

Replay missed events

Browsers reconnect with the Last-Event-ID header. Read it before subscribing to live events:

if lastID, ok := stream.LastEventID(); ok {
    for _, event := range c.Notifications.Since(lastID) {
        if err := stream.Send(event.SSEEvent()); err != nil {
            return err
        }
    }
}

SSE streams bypass dynamic route ETags because the response body is written incrementally.