Skip to content

Release 2.4.1: SSE connections now respect middleware headers#65

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

Release 2.4.1: SSE connections now respect middleware headers#65
dcrockwell merged 2 commits intomainfrom
develop

Conversation

@dcrockwell
Copy link
Copy Markdown
Contributor

Release 2.4.1

Release Date: March 11, 2026

Why

SSE connections were silently dropping headers added by middleware (CORS, security headers, etc.), causing cross-origin EventSource requests to fail. This was a regression from the 2.4.0 SSE feature release — the Mist SSE upgrade happened inside the controller before middleware could modify the response, so any headers added by middleware were already on the wire without them.

What's Fixed

upgrade_to_sse now defers the Mist SSE upgrade until after the middleware chain completes. A new "deferred upgrade" pattern stashes an upgrade thunk in the process dictionary, lets middleware run on the dummy response, then the handler extracts headers from the middleware-modified response and performs the upgrade with them.

If middleware returns a non-200 status (e.g., 401 from auth), the SSE upgrade is not performed and the error response is returned normally.

What's Included

  • Core fix across sse.gleam, handler.gleam, and internal.gleam
  • 4 new SSE integration test scenarios (CORS, middleware stacking, rejection, streaming continuity)
  • CORS/security/rejection middleware examples in examples/sse/
  • "Using Middleware with SSE" section added to the SSE guide
  • Updated changelog and release notes

Test Results

  • 257 core unit tests pass
  • 8 SSE integration tests pass (3 existing + 4 new + 1 runner)
  • 9 WebSocket integration tests pass (no regression)

Full Release Notes

See releases/release-2.4.1.md

dcrockwell and others added 2 commits March 10, 2026 22:11
## Why This Change Was Made
- `upgrade_to_sse` was calling `mist.server_sent_events` inside the controller,
  which sends HTTP headers to the TCP socket immediately — before middleware has
  a chance to modify the response. This meant CORS headers, security headers, and
  any other middleware-applied headers were silently discarded, causing cross-origin
  SSE requests to fail with CORS errors.

## What Was Changed
- Added `upgrade_key` constant to `internal.gleam` for a new deferred upgrade stash
- Changed `sse.gleam` to stash an upgrade thunk (a closure that will perform the
  Mist SSE upgrade) instead of calling `mist.server_sent_events` directly
- Changed `handler.gleam` to check for the stashed upgrade thunk after
  `execute_route` returns the middleware-modified Dream response; if found and
  status is 200, it extracts headers and calls the thunk with them
- Added `extract_dream_headers` helper to convert Dream headers to tuples
- Added CORS, rejection, and security-headers middleware to the SSE example app
- Added 4 new integration test scenarios: CORS headers, middleware stacking,
  middleware rejection (401 blocks upgrade), and streaming continuity
- Added "Using Middleware with SSE" section to the SSE guide
- Bumped version to 2.4.1

## Note to Future Engineer
- The `upgrade_key` stash is separate from `response_key` — WebSocket still uses
  `response_key` in the outer handler function. Don't merge them or you'll break
  one of the two protocols. Yes, having two different stash-and-upgrade mechanisms
  for two different protocols in the same handler is a bit of an architectural
  smell, but Mist's `websocket` doesn't accept an `initial_response` parameter so
  the WebSocket side can't use this deferred pattern without an upstream change.
  Enjoy that asymmetry.
- The status check (`200 -> upgrade, _ -> convert`) handles the case where
  middleware calls `next` (which stashes the thunk) but then overrides the
  response with a non-200. Don't remove it thinking "middleware rejection already
  prevents the controller from running" — that's only true for middleware that
  short-circuits before calling `next`.
SSE connections now respect middleware headers like CORS
@dcrockwell dcrockwell self-assigned this Mar 11, 2026
@dcrockwell dcrockwell added bug Something isn't working release Official public releases labels Mar 11, 2026
@dcrockwell dcrockwell merged commit d2f5579 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 release Official public releases

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant