Skip to content

Release 2.4.0: Native Server-Sent Events support#63

Merged
dcrockwell merged 4 commits intomainfrom
develop
Mar 11, 2026
Merged

Release 2.4.0: Native Server-Sent Events support#63
dcrockwell merged 4 commits intomainfrom
develop

Conversation

@dcrockwell
Copy link
Copy Markdown
Contributor

Release 2.4.0

Adds native Server-Sent Events (SSE) support backed by dedicated OTP actors, fixing a critical bug where SSE streams stalled after 2–3 events.

What's in this release

New: dream/servers/mist/sse module

  • upgrade_to_sse upgrades HTTP requests to SSE connections, each with a dedicated OTP actor and mailbox
  • Structured event builders (event, event_name, event_id, event_retry) with pipeline syntax
  • Actor lifecycle control (continue_connection, continue_connection_with_selector, stop_connection)
  • Follows the same stash-and-upgrade pattern and no-closures philosophy as the WebSocket module

Bug fix: SSE streams no longer stall

The old sse_response used chunked transfer encoding, which blocked in the mist handler process's mailbox competing with TCP messages. The new module gives each SSE connection its own OTP actor, completely eliminating the contention.

Deprecated: response.sse_response

Marked deprecated with migration guidance. Not removed — existing code continues to compile.

Documentation and testing

  • New docs/guides/sse.md guide
  • Updated streaming API reference
  • Unit tests, tested documentation snippets, and a full example app with Cucumber integration tests
  • Release notes at releases/release-2.4.0.md

Commits

  • cad9983 feat: add native SSE support via dedicated OTP actors
  • 679853d fix: use health endpoint for SSE integration test readiness check
  • a982444 fix: remove synchronous GET to SSE endpoint in integration tests

Changelog

See CHANGELOG.md for the full list of additions and deprecations.

dcrockwell and others added 4 commits March 10, 2026 19:57
## Why This Change Was Made
- The existing `sse_response` function used chunked transfer encoding (`Stream(yielder)`) which Mist converted to `Chunked(yielder)`. The yielder blocked in the mist handler process's mailbox, competing with TCP messages (ACKs, window updates), causing SSE streams to stall after 2-3 events.
- A proper SSE implementation requires a dedicated OTP actor per connection with its own mailbox, matching how Mist handles WebSockets.

## What Was Changed
- Added `src/dream/servers/mist/sse.gleam` module with `upgrade_to_sse`, `send_event`, event builders (`event`, `event_name`, `event_id`, `event_retry`), and action helpers (`continue_connection`, `continue_connection_with_selector`, `stop_connection`)
- Follows the same stash-and-upgrade pattern as `websocket.gleam` — retrieves the raw Mist request from the process dictionary, calls `mist.server_sent_events`, stashes the resulting response
- Deprecated `response.sse_response` with doc comment directing users to the new module
- Added unit tests, tested documentation snippets, and a full `examples/sse/` example app with Cucumber integration tests
- Added `docs/guides/sse.md` guide, updated `docs/reference/streaming-api.md` and `docs/guides/streaming.md`
- Bumped version to 2.4.0, updated CHANGELOG.md, created release notes

## Note to Future Engineer
- Dream's `SSEConnection` wraps Mist's opaque `SSEConnection` which wraps an internal `Connection(body, socket, transport)`. If you ever need raw socket access (e.g., for `send_raw`), capture socket/transport from `mist_request.body` at upgrade time rather than cracking open the opaque type later. Your future self will thank you when Mist bumps a minor version.
- The old `sse_response` is deprecated but not removed. If you're reading this in 2028 wondering why it's still here — congratulations, you found the tech debt. The `data:` prefix adds itself, like a clingy coworker who cc's themselves on every email.
## Why This Change Was Made
- CI was hanging indefinitely during SSE integration tests because the readiness check curled the `/events` SSE endpoint, which is a long-lived stream that never closes
- `curl -s http://localhost:8081/events` connects, receives the 200 + headers, then blocks forever waiting for the stream to end — so the readiness loop never advances

## What Was Changed
- Added a `/health` endpoint to the SSE example router that returns a plain 200 "ok" response
- Changed the Makefile readiness check from `/events` to `/health`

## Note to Future Engineer
- Every other example curls a normal HTTP endpoint for readiness. SSE and WebSocket endpoints are long-lived — never use them for health checks unless you enjoy watching CI spin like a loading screen from 2005.
## Why This Change Was Made
- CI was hanging because the "SSE endpoint returns correct headers" scenario did a synchronous `HTTPoison.get` to the `/events` SSE endpoint
- SSE streams never end, and the server sends events every second, so `recv_timeout` never fires (data IS being received) — the call hangs forever

## What Was Changed
- Merged the header assertions into the "SSE endpoint streams events" scenario, which already uses async streaming
- The SSE connect step now captures response headers from `AsyncHeaders` instead of discarding them
- Added a new step definition `the SSE response header {string} should contain {string}` for header assertions on async SSE connections
- Removed the standalone headers scenario that used synchronous HTTP

## Note to Future Engineer
- Never use synchronous HTTP requests to test SSE or WebSocket endpoints. They stream forever. Use async connections and check headers from the handshake phase.
- If you're tempted to add `recv_timeout: 1_000` as a "fix" — it won't work. The server keeps sending data, so recv_timeout resets with every event. It's not a timeout problem, it's a "this stream literally never ends" problem.
Fix SSE streams stalling by using dedicated OTP actors
@dcrockwell dcrockwell self-assigned this Mar 11, 2026
@dcrockwell dcrockwell added bug Something isn't working enhancement New feature or request release Official public releases labels Mar 11, 2026
@dcrockwell dcrockwell merged commit 924263c into main Mar 11, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working enhancement New feature or request release Official public releases

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant