Releases: TrustBound/dream
2.4.0
Dream 2.4.0 Release Notes
Release Date: March 10, 2026
Dream 2.4.0 introduces native Server-Sent Events (SSE) support backed by dedicated OTP actors, fixing a critical bug where SSE streams would stall after a few events. The new dream/servers/mist/sse module follows the same stash-and-upgrade pattern as WebSockets, giving each SSE connection its own mailbox and eliminating TCP message contention.
Key Highlights
- Native SSE support —
upgrade_to_ssespawns a dedicated OTP actor per connection, replacing the broken chunked encoding approach - Builder-style event API —
event,event_name,event_id,event_retryfor structured SSE events - No closures — explicit
dependenciesparameter, consistent with Dream's WebSocket pattern - Deprecation of
sse_response— old function kept for compatibility but marked deprecated with migration guidance - Comprehensive testing — unit tests, tested documentation snippets, and Cucumber integration tests
- Full documentation — new SSE guide, updated streaming API reference, example application
What's New
Native SSE Module: dream/servers/mist/sse
The new module provides Dream's SSE abstraction for the Mist server adapter. It upgrades an HTTP request to an SSE connection backed by a dedicated OTP actor, avoiding the stalling issues of chunked transfer encoding.
import dream/servers/mist/sse
pub fn handle_events(request, _context, _services) {
sse.upgrade_to_sse(
request,
dependencies: MyDeps,
on_init: handle_init,
on_message: handle_message,
)
}New Types:
sse.SSEConnection— opaque handle to the SSE connectionsse.Event— structured SSE event built with builder functionssse.Action(state, message)— controls the SSE actor lifecycle
Event Builders:
sse.event("{\"count\": 42}")
|> sse.event_name("tick")
|> sse.event_id("42")
|> sse.event_retry(5000)Action Helpers:
sse.continue_connection(state)— keep the actor runningsse.continue_connection_with_selector(state, selector)— keep running with a new selectorsse.stop_connection()— shut down the actor
SSE Example Application
Added examples/sse/ with a complete ticker application:
- Real-time counter events streamed to connected clients
- Named events with IDs for client-side filtering and reconnection
- Cucumber/Gherkin integration tests using HTTPoison's async streaming
- Scenario verifying events stream continuously without stalling
SSE Documentation
- New
docs/guides/sse.mdcovering SSE vs WebSockets vs streaming, the full upgrade lifecycle, event builders, broadcasting withSubjectandbroadcaster, client-sideEventSource, and testing - Updated
docs/reference/streaming-api.mdwithupgrade_to_sseAPI reference, event builder signatures, and action helper documentation - Updated
docs/guides/streaming.mdto direct users to the new dedicated SSE guide
Bug Fix
SSE Streams Stalling After 2–3 Events
Bug: response.sse_response used Stream(yielder) which Mist converted to chunked transfer encoding. The yielder blocked in the mist handler process's mailbox, competing with TCP messages (ACKs, window updates). After 2–3 events the mailbox would back up and the stream would stall.
Fix: The new sse.upgrade_to_sse function calls Mist's native server_sent_events which spawns a dedicated OTP actor with its own mailbox. SSE events and TCP messages no longer contend for the same process, eliminating the stalling entirely.
Deprecated
response.sse_response
The sse_response function is deprecated. It remains available for backward compatibility but its doc comment now includes a deprecation warning directing users to dream/servers/mist/sse.upgrade_to_sse.
Migration: Replace sse_response with upgrade_to_sse:
// Before (stalls after a few events)
import dream/http/response
pub fn handle_events(request, _context, _services) {
response.sse_response(200, my_yielder, "text/event-stream")
}
// After (dedicated actor, no stalling)
import dream/servers/mist/sse
pub fn handle_events(request, _context, _services) {
sse.upgrade_to_sse(
request,
dependencies: MyDeps,
on_init: handle_init,
on_message: handle_message,
)
}Testing
- Unit tests for event builders and action wrappers in
test/dream/servers/mist/sse_test.gleam - Tested documentation snippets in
test/snippets/ensuring all code examples from docs compile and run - Cucumber integration tests in
examples/sse/verifying:- Correct SSE response headers (
Content-Type: text/event-stream) - Events stream with expected data payloads
- Named events include
event:andid:fields - Events do not stall (the original bug scenario)
- Correct SSE response headers (
Upgrading
Update your dependencies:
[dependencies]
dream = ">= 2.4.0 and < 3.0.0"Then run:
gleam deps downloadMigration Guide
No breaking changes! This release is fully backward compatible.
If you are currently using sse_response, we strongly recommend migrating to the new SSE module:
-
Import the SSE module:
import dream/servers/mist/sse
-
Define your message type and dependencies:
pub type MyMessage { Tick } pub type MyDeps { MyDeps }
-
Replace
sse_responsewithupgrade_to_sse:pub fn handle_events(request, _context, _services) { sse.upgrade_to_sse( request, dependencies: MyDeps, on_init: handle_init, on_message: handle_message, ) }
-
See
examples/sse/anddocs/guides/sse.mdfor a complete walkthrough.
Documentation
- dream - v2.4.0
- SSE Guide
- Streaming API Reference
Community
Full Changelog: CHANGELOG.md
2.3.3
Dream 2.3.3 Release Notes
Release Date: March 7, 2026
This release fixes a bug where multiple Set-Cookie headers were collapsed into a single header, causing browsers to only receive the last cookie.
Key Highlights
- Set-Cookie fix: Multiple cookies now each produce their own
Set-Cookieheader per RFC 6265 - Comprehensive test coverage: 7 new tests validate multi-cookie behavior and RFC compliance
Fixed
The mist response converter used set_header (which replaces existing headers with the same name) for every header, including Set-Cookie. RFC 6265 requires each cookie to be sent as a separate Set-Cookie header — browsers do not parse comma-separated Set-Cookie values.
add_header now uses prepend_header (which allows duplicates) for set-cookie headers, and set_header (which replaces) for everything else. This matches the Gleam standard library's own set_cookie convention.
Before
fn add_header(acc, header) {
http_response.set_header(acc, header.0, header.1)
}After
fn add_header(acc, header) {
case header.0 {
"set-cookie" -> http_response.prepend_header(acc, header.0, header.1)
_ -> http_response.set_header(acc, header.0, header.1)
}
}Added
count_mist_headerstest matcher for verifying header counts by nameextract_all_mist_header_valuestest matcher for extracting all values of a header- 7 new tests covering:
- Multiple cookies produce separate
Set-Cookieheaders - Each cookie value is individually present
- Three cookies produce three headers
- Manual
Set-Cookiein headers coexists with cookies from thecookiesfield - Duplicate non-cookie headers are still deduplicated
- Cookies with attributes each get their own header
- Cookies alongside other headers don't interfere
- Multiple cookies produce separate
Upgrading
Update your dependencies:
[dependencies]
dream = ">= 2.3.3 and < 3.0.0"Then run:
gleam deps downloadDocumentation
- dream - v2.3.3
2.3.2
Dream 2.3.2 Release Notes
Release Date: February 4, 2026
This release focuses on startup clarity and smoother local testing.
Key Highlights
- Fail-fast startup: Servers now report a clear error when a port is already in use
- Local test parity with CI: One Makefile target prepares the integration databases
- Cleaner server startup flow: Internal helpers keep the public API simple
Thanks
Thanks to daniellionel01 for reporting the issue.
Fixed
Dream now detects port conflicts before starting the Mist server and returns a
direct, actionable error message. No more “it started” when it didn’t.
Added
A new setup-integration-dbs target prepares the example databases and
applies migrations, matching CI’s database setup steps.
Upgrading
Update your dependencies:
[dependencies]
dream = ">= 2.3.2 and < 3.0.0"Then run:
gleam deps downloadDocumentation
- dream - v2.3.2
2.3.1
Dream 2.3.1 Release Notes
Release Date: December 13, 2025
This is not “just a patch release”.
This is the moment web development looks back on and quietly says: “…yeah, that changed everything.”
Historians will argue where the modern era began.
Some will cite the printing press.
Some will cite the internet.
They will be wrong.
The modern era began when we stopped writing 418 like we were chiseling responses into stone tablets, and started writing the only thing that was ever true:
teapot.
Key Highlights
- A new era of human-readable computing:
teapotexists, and your codebase can finally stop pretending it enjoys numerals - RFC 2324 alignment: Dream now stands on the shoulders of giants (and the shoulders are holding coffee)
- Massively improved signal-to-number ratio: one less “magic number”, one more semantic truth
- Societal impact: code review comments reduced by up to 418% (this statistic is obviously real because it contains a number)
The Feature That Will Be Remembered
We added the single most important public constant in the history of constants:
teapot = 418
Yes, it’s “just” an Int.
So is gravity.
Before (the dark ages)
text_response(418, "No.")After (enlightenment)
import dream/http/response.{text_response}
import dream/http/status.{teapot}
pub fn brew_coffee(_request, _context, _services) {
text_response(teapot, "Server refuses to brew coffee because it is, in fact, a teapot.")
}Your future self will weep with gratitude.
Your teammates will weep with gratitude.
Somewhere, an editor linter will briefly consider smiling.
Compatibility
This is fully backwards-compatible.
You can keep using 418 if you absolutely insist on living dangerously.
Upgrading
Update your dependencies:
[dependencies]
dream = ">= 2.3.1 and < 3.0.0"Then run:
gleam deps downloadDocumentation
- dream - v2.3.1
Full Changelog: CHANGELOG.md
2.3.0
Dream 2.3.0 Release Notes
Release Date: December 1, 2025
Dream 2.3.0 upgrades to dream_test for a better testing experience, and fixes two router bugs that were discovered during the migration.
Key Highlights
- 🧪 Test Framework Upgrade - Switched to
dream_testfor BDD-style testing with process isolation - 🐛 Router Parameter Fix - Parameters now returned in correct path order
- 🔧 Extension Pattern Fix - Patterns like
/products.{json,xml}now work correctly - 📚 Testing Documentation - Complete rewrite for
dream_testframework
Why We Switched to dream_test
dream_test provides features that gleeunit doesn't offer:
| Feature | What You Get |
|---|---|
| BDD-style syntax | describe/it blocks that read like documentation |
| Process isolation | Each test runs in its own BEAM process |
| Lifecycle hooks | before_each, after_each, before_all, after_all |
| Chainable matchers | should() |> be_ok() |> equal(expected) |
| Parallel execution | Tests run concurrently across cores |
| Crash isolation | One test crashing doesn't affect others |
| Timeout protection | Hanging tests get killed automatically |
What's Changed
Test Framework Migration
Replaced gleeunit with dream_test across all 21 test files:
Before (gleeunit):
pub fn my_test_test() {
my_function()
|> should.equal(expected)
}After (dream_test):
pub fn tests() -> UnitTest {
describe("my_module", [
describe("my_function", [
it("does X when Y", fn() {
// Arrange
let input = create_input()
// Act
let result = my_function(input)
// Assert
result
|> should()
|> equal(expected)
|> or_fail_with("Expected X")
}),
]),
])
}New Infrastructure:
- 21 custom matchers in
test/matchers/for cleaner assertions - Reusable fixtures in
test/fixtures/(request, response, handler, hooks) - Process isolation for all 239 tests
- Self-identifying benchmark output for parallel execution
Router Bug Fixes
Parameter Ordering Fixed:
Parameters are now returned in the order they appear in the path:
// Route: /users/:user_id/posts/:post_id
// Request: /users/1/posts/2
// Before (2.2.0): [("post_id", "2"), ("user_id", "1")] <- WRONG!
// After (2.3.0): [("user_id", "1"), ("post_id", "2")] <- CorrectExtension Pattern Matching Fixed:
Added LiteralExtension segment type for patterns like /products.{json,xml}:
// Route definition
route(Get, "/products.{json,xml}", products_controller, [])
// Before (2.2.0): /products.json -> No match
// After (2.3.0): /products.json -> Match!Documentation Updates
- Rewrote
docs/guides/testing.md- Complete guide fordream_test - Rewrote
docs/contributing/testing.md- Contributor testing guidelines - Updated streaming guide test examples
Upgrading
Update your dependencies:
[dependencies]
dream = ">= 2.3.0 and < 3.0.0"Then run:
gleam deps downloadMigration Notes
If you depended on reversed parameter order:
If your code was working around the reversed parameter order bug, you'll need to update it. The correct order is now returned.
If you had extension patterns that weren't matching:
Routes like /products.{json,xml} now work as expected. No code changes needed unless you had workarounds.
Internal Changes
- Removed
gleeunitdependency - Added
dream_test >= 1.0.3dev dependency - Added
dream_http_client >= 2.1.1dev dependency (for test server polling) - New
LiteralExtensionsegment type in router trie
Testing
All 239 tests pass with the new framework:
Summary: 239 run, 0 failed, 239 passed
Test categories:
- Router: 85 tests (routing, params, wildcards, extensions, middleware)
- HTTP: 98 tests (request, response, headers, cookies, validation)
- Server: 24 tests (mist handler, request, response, lifecycle)
- Streaming: 18 tests (body parsing, middleware, error handling)
- Benchmarks: 6 tests (router performance)
Documentation
- dream - v2.3.0
- dream_test - Testing framework docs
- Testing Guide
Community
Full Changelog: CHANGELOG.md
2.2.0
Dream 2.2.0 Release Notes
Release Date: November 25, 2025
Dream 2.2.0 introduces comprehensive WebSocket support with a pub/sub broadcaster service, making it easy to build real-time applications with full server abstraction. This release also improves encapsulation of the Dream type, fixes several configuration bugs, and includes a 1,270+ line educational example with extensive integration tests.
Key Highlights
- 🎯 WebSocket Support - Server-agnostic WebSocket API with no vendor lock-in
- 📡 Pub/Sub Broadcaster - Generic OTP-based broadcaster service for real-time messaging
- 🔒 Dream Type Encapsulation - Opaque type prevents coupling to internal implementation
- 🔍 Configuration Accessors - New functions to inspect Dream instance settings
- 🐛 Bug Fixes - Fixed
bind()configuration persistence and type leakage issues - 📚 Comprehensive Example - 1,270+ line README with complete WebSocket chat app
- ✅ Integration Tests - 9 Cucumber scenarios with CI/CD integration
- 📖 Documentation Overhaul - Beginner-friendly rewrite with standardized terminology and examples
- ⚙️ Sensible Defaults - 10MB max body size (was effectively infinite)
What's New
🆕 WebSocket Support
Dream now includes comprehensive WebSocket support with a server-agnostic API that completely abstracts away the underlying Mist implementation.
Key Features:
- Upgrade HTTP requests to WebSocket connections
- Handle text messages, binary messages, and custom application messages
- Full connection lifecycle management (init, message, close)
- Server-agnostic design - users never see Mist types
- No closures required - explicit dependencies pattern
- Pub/sub broadcasting service for fan-out messaging
New Module: dream/servers/mist/websocket
import dream/servers/mist/websocket
pub fn handle_chat_upgrade(request, context, services) {
let dependencies = ChatDependencies(user: "Alice", services: services)
websocket.upgrade_websocket(
request,
dependencies: dependencies,
on_init: handle_init,
on_message: handle_message,
on_close: handle_close,
)
}WebSocket Types:
websocket.Connection- Opaque connection type (hides Mist)websocket.Message(custom)- Text, Binary, Custom, or ConnectionClosedwebsocket.Action(state, custom)- Continue or stop connectionwebsocket.SendError- Dream's own error type (not Mist's)
New Module: dream/services/broadcaster
A publish/subscribe broadcaster service for WebSocket and general fan-out messaging:
import dream/services/broadcaster
// Start a broadcaster at application startup
let assert Ok(chat_bus) = broadcaster.start_broadcaster()
// Subscribe to receive messages (typically in WebSocket init)
let channel = broadcaster.subscribe(chat_bus)
let selector = broadcaster.channel_to_selector(channel)
// Publish messages to all subscribers
broadcaster.publish(chat_bus, ChatMessage("Hello!"))Broadcaster Features:
- Generic OTP actor-based pub/sub service
- Type-safe message distribution to multiple consumers
- Integrates seamlessly with WebSocket selectors
- Automatic subscriber cleanup on process termination
- Suitable for chat rooms, notifications, live updates, etc.
- Building block for more complex routing/partitioning logic
Complete Example:
See examples/websocket_chat/ for a full-featured chat application with:
- Real-time messaging with pub/sub broadcasting
- Join/leave notifications
- Multiple concurrent users
- Comprehensive integration tests
- 1,270+ line README with step-by-step walkthrough
- Complete frontend implementation (JavaScript, CSS)
- Cucumber/Gherkin BDD-style integration tests
🔍 Dream Type Accessors
Added public accessor functions to inspect Dream instance configuration:
import dream/dream
let app = server.new()
|> server.max_body_size(5_000_000)
|> server.bind("0.0.0.0")
// Get configuration
let max_size = dream.get_max_body_size(app) // Returns 5_000_000
case dream.get_bind_interface(app) {
Some(interface) -> // Interface is configured
None -> // Using default
}Available Accessors:
dream.get_router()- Get configured routerdream.get_context()- Get configured contextdream.get_services()- Get configured servicesdream.get_max_body_size()- Get max body size limitdream.get_bind_interface()- Get configured bind interfacedream.get_server()- Get underlying server (requiresrisks_understood: True)
get_server():
The get_server() function requires explicit acknowledgment of risks:
// You MUST pass risks_understood: True
let mist_server = dream.get_server(app, risks_understood: True)This function exposes Mist types and breaks Dream's abstraction. Passing False will panic. This is intentional - it forces you to consciously acknowledge that you're coupling your code to Mist.
Improvements
Dream Type Encapsulation
The Dream type is now opaque, preventing users from depending on internal server implementation details:
- Users cannot construct
Dreamvalues directly - Users cannot access internal fields directly
- All interaction is through builder functions and accessors
- Prevents accidental coupling to Mist types
- Internal code uses
dream.create()with labeled arguments for construction - Framework provides controlled access through accessor functions
Why This Matters:
- Ensures backward compatibility for future changes to internal structure
- Makes it impossible to accidentally couple application code to Mist
- Provides cleaner separation between Dream's public API and internal implementation
- Allows Dream to change underlying server implementations without breaking user code
This is not a breaking change - users never constructed Dream directly, only through server.new().
Sensible Defaults
- Max body size: Changed from effectively infinite (max_int64) to 10MB (10,000,000 bytes)
- Prevents accidental memory exhaustion from unbounded request bodies
- Can be overridden with
server.max_body_size()for specific use cases
Internal Improvements
Dream Type Construction:
- Added internal
dream.create()constructor with labeled arguments - All internal Dream construction now uses explicit labeled parameters
- Improves maintainability and reduces errors from positional argument changes
- Provides single source of truth for Dream instance creation
Dependency Management:
- Removed
glistendependency (was only used transiently viamist) - Dream now has zero direct dependencies on lower-level server libraries
- All server types are properly abstracted through Dream's API
Bug Fixes
Fixed bind() Configuration Persistence
Bug: Calling server.bind("0.0.0.0") before server.listen() would lose the bind configuration.
Fix: Bind interface is now stored in the Dream type and properly applied during listen().
// Now works correctly!
server.new()
|> server.router(my_router)
|> server.bind("0.0.0.0") // This configuration persists
|> server.listen(3000) // Server binds to 0.0.0.0Fixed Vendor Lock-In in WebSocket API
Bug: WebSocket API was leaking Mist and Glisten types (mist.Text, glisten.SocketReason).
Fix: All types are now abstracted through Dream:
websocket.TextMessageinstead ofmist.Textwebsocket.SendErrorinstead ofglisten.SocketReasonwebsocket.Connection(opaque) instead ofmist.WebsocketConnection
Users never see Mist types, maintaining Dream's server-agnostic design.
Documentation
Beginner-Friendly Documentation Overhaul
Completely revised and standardized all documentation to be more accessible for developers new to Dream:
Consistency Improvements:
- Standardized terminology across all documentation files
- Aligned code examples to match actual implementation patterns
- Fixed inconsistencies between different sections explaining the same concepts
- Updated example READMEs with consistent formatting and structure
- Ensured server implementation examples in docs match actual server code
Enhanced Learning Resources:
- Improved quickstart guide with clearer explanations
- Updated learning tutorials (hello-world, building-api, auth, advanced patterns)
- Enhanced guides for controllers, templates, testing, streaming, and operations
- Better architecture and design principle documentation
- More detailed examples documentation
Example Code Improvements:
- Updated rate_limiter example with better patterns
- Enhanced static file serving example documentation
- Improved streaming_capabilities example clarity
- All examples now follow consistent patterns and conventions
Impact:
- ~830 lines added, ~374 lines removed across 24 documentation files
- Every major documentation section touched for consistency
- Removed confusing terminology and ambiguous explanations
- Made it easier for newcomers to understand Dream's patterns
WebSocket Chat Example
Added comprehensive WebSocket example with extensive documentation:
- 1,270+ line README covering:
- What WebSockets are and when to use them (vs. polling, SSE, etc.)
- Complete architectural overview with data flow diagrams
- Step-by-step walkthrough of every component (services, controllers, views, templates)
- Message lifecycle from user input → WebSocket → pub/sub → all clients
- Frontend JavaScript implementation explained line-by-line
- CSS styling and modern UI patterns explained
- Deep dive into Dream's no-closure pattern and why it matters
- Server-agnostic design philosophy and abstraction benefits
- Common patterns: room management, user presence, message hist...
2.1.0
Dream 2.1.0 Release Notes
Release Date: November 24, 2025
Dream 2.1.0 introduces the new dream_mock_server module for testing HTTP clients and a major overhaul of dream_http_client with message-based streaming support, making it fully compatible with OTP actors and concurrent operations.
What's New
🆕 dream_mock_server 1.0.0
A general-purpose HTTP mock server for testing HTTP clients without external dependencies. Provides both streaming and non-streaming endpoints with programmatic control.
Key Features:
- Programmatic control:
start(port)andstop(handle)functions - 10 non-streaming endpoints for various test scenarios
- 4 streaming endpoints for testing chunked responses
- Full Cucumber integration test suite
- Comprehensive HexDocs documentation
Non-Streaming Endpoints:
GET /- Info page with endpoint documentationGET /get- Echo query parametersPOST /post- Echo request bodyGET /status/:code- Return specified HTTP status codeGET /json- Sample JSON responseGET /text- Plain text responseGET /uuid- Generate and return UUIDGET /large- Large response (1MB) for memory testingGET /empty- Empty response bodyGET /slow- Delayed response (2s) for timeout testing
Streaming Endpoints:
GET /stream/fast- 10 chunks at 100ms intervalsGET /stream/slow- 5 chunks at 2s intervalsGET /stream/burst- Random burst pattern (5-10 chunks, 100-500ms delays)GET /stream/huge- 100 chunks for memory/performance testing
Example Usage:
import dream_mock_server/server
pub fn test_http_client() {
let assert Ok(mock) = server.start(3004)
// Make requests to localhost:3004
let result = http_client.get("http://localhost:3004/json")
server.stop(mock)
}🚨 dream_http_client 2.0.0 - BREAKING CHANGES
Major overhaul with message-based streaming, OTP actor support, and comprehensive error handling improvements.
Breaking Change 1: Module Consolidation
The fetch and stream modules have been consolidated into a unified client module.
Migration:
// Before (v1.x)
import dream_http_client/fetch
import dream_http_client/stream
let body = client.new
|> client.host("api.example.com")
|> fetch.request()
let chunks = client.new
|> client.host("cdn.example.com")
|> stream.stream_request()
// After (v2.0)
import dream_http_client/client
let body = client.new
|> client.host("api.example.com")
|> client.send()
let chunks = client.new
|> client.host("cdn.example.com")
|> client.stream_yielder()Breaking Change 2: stream_yielder() Return Type
The stream_yielder() function now returns Result values to properly surface errors.
Migration:
// Before (v1.x)
client.new
|> host("api.example.com")
|> stream.stream_request()
|> yielder.each(fn(chunk) {
process(chunk)
})
// After (v2.0)
client.new
|> host("api.example.com")
|> client.stream_yielder()
|> yielder.each(fn(result) {
case result {
Ok(chunk) -> process(chunk)
Error(reason) -> handle_error(reason)
}
})Breaking Change 3: StreamMessage Type
Added DecodeError variant to StreamMessage for explicit FFI corruption handling.
Migration:
// If you pattern match on StreamMessage, add DecodeError handling:
case msg {
StreamStart(req_id, headers) -> ...
Chunk(req_id, data) -> ...
StreamEnd(req_id, headers) -> ...
StreamError(req_id, reason) -> ...
DecodeError(reason) -> ... // NEW - handle FFI corruption
}
// Or use a catch-all:
case msg {
Chunk(req_id, data) -> process(data)
_ -> continue(state)
}New Features
Message-Based Streaming (dream_http_client 2.0.0)
Added full OTP actor support for concurrent HTTP streams.
New API:
stream_messages()- Start a message-based stream, returnsRequestIdStreamMessagetype - Stream lifecycle messages sent to your mailboxselect_stream_messages()- Integrate with OTP selectorscancel_stream()- Cancel active streams
Example:
import dream_http_client/client.{
type StreamMessage, Chunk, StreamEnd, StreamError, StreamStart,
select_stream_messages
}
import gleam/otp/actor.{continue}
import gleam/erlang/process.{new_selector}
pub type Message {
HttpStream(StreamMessage)
}
fn init_selector() {
new_selector()
|> select_stream_messages(HttpStream)
}
fn handle_message(msg: Message, state: State) {
case msg {
HttpStream(Chunk(req_id, data)) -> process_chunk(data, state)
HttpStream(StreamEnd(req_id, _)) -> cleanup(req_id, state)
HttpStream(StreamError(req_id, reason)) -> handle_error(state)
HttpStream(StreamStart(_, _)) -> continue(state)
HttpStream(DecodeError(reason)) -> log_corruption(reason, state)
}
}Configurable Timeout (dream_http_client 2.0.0)
All HTTP requests now support configurable timeouts.
Example:
import dream_http_client/client.{timeout}
client.new
|> host("slow-api.example.com")
|> timeout(5000) // 5 second timeout
|> send()Default timeout is 600 seconds (10 minutes) if not specified.
Improved Error Handling (dream_http_client 2.0.0)
All errors now include full context:
- No more discarded errors (
Error(_)patterns eliminated) - Decode errors include underlying reasons
- All error messages are actionable and user-friendly
- No
paniccalls - all errors return gracefully
Before:
// Errors were silently discarded or returned as generic messages
Error("Request failed")After:
// Errors include full context
Error("Failed to decode headers: Expected string, found integer at position 2")
Error("Stream failed: Connection timeout after 30000ms")Documentation
New Governance Files
CONTRIBUTING.md- Quick start guide for contributorsCODE_OF_CONDUCT.md- Community standards (Contributor Covenant 2.1)SECURITY.md- Security policy clarifying Dream's responsibilities as a library- GitHub issue templates (bug reports, feature requests)
- GitHub pull request template
Documentation Improvements
- Moved
TESTING.mdtodocs/contributing/testing.md - Updated architecture docs with correct
dream_http_clientAPI - Added "About Dream" section explaining TrustBound's role
- All
dream_http_clientexamples updated to use new API
Bug Fixes
dream_http_client 2.0.0
- Fixed
send()for non-streaming requests: Now uses synchronoushttpcmode. Previously used streaming mode which couldn't handleContent-Lengthresponses - Fixed URL port handling:
stream_yielder()andstream_messages()now correctly include port in URLs - Fixed message routing: Streaming messages now sent to correct process
- Fixed header decoding: Errors propagate instead of being hidden
- Fixed RequestId handling: FFI corruption returns
DecodeErrorvariant instead of crashing
Testing
dream_http_client 2.0.0
- All tests now use
dream_mock_serverinstead of external dependencies - Configurable test port via
MOCK_SERVER_PORTenvironment variable - Added unit tests for malformed header handling
- Added unit test for
timeout()builder function
dream_mock_server 1.0.0
- Comprehensive Cucumber integration test suite
- Unit tests for port configuration and lifecycle
- Tests for multiple concurrent servers
- Tests for server restart and idempotent stop
Internal Changes
Refactored FFI Boundary (dream_http_client 2.0.0)
Erlang now handles all raw httpc message parsing and normalization:
- Erlang FFI performs pattern matching on raw messages
- Erlang normalizes headers (charlist to binary)
- Gleam receives clean, simplified data structures
- Clear separation of concerns between layers
Code Quality (dream_http_client 2.0.0)
- Flattened all nested
caseexpressions - Extracted all anonymous functions to named helpers
- Consistent error handling patterns throughout
- Comprehensive inline documentation
Upgrading
Update your dependencies:
[dependencies]
dream = ">= 2.1.0 and < 3.0.0"
dream_http_client = ">= 2.0.0 and < 3.0.0"
[dev-dependencies]
dream_mock_server = ">= 1.0.0 and < 2.0.0"Then run:
gleam deps downloadMigration Checklist
- ✅ Update imports:
dream_http_client/fetch→dream_http_client/client - ✅ Update imports:
dream_http_client/stream→dream_http_client/client - ✅ Rename function calls:
fetch.request()→client.send() - ✅ Rename function calls:
stream.stream_request()→client.stream_yielder() - ✅ Update
stream_yielder()usage to handleResulttype - ✅ Add
DecodeErrorhandling toStreamMessagepattern matches (or use catch-all) - ✅ Consider using
stream_messages()for OTP actors with concurrent streams - ✅ Replace external mock server dependencies with
dream_mock_server - ✅ Run
gleam testto verify all tests pass
Acknowledgements
Special thanks to Louis Pilfold for bringing to our attention that the HTTP client did not support message-based streaming for OTP actors. This feedback led to the comprehensive overhaul of dream_http_client in this release, making it fully compatible with OTP patterns and concurrent operations.
Documentation
All packages are available with updated documentation on HexDocs:
- dream - v2.1.0
- dream_http_client - v2.0.0
- dream_mock_server - v1.0.0 (NEW)
- dream_config
- dream_postgres
- [dream_opensearch](https://hexdocs.pm/dream_opensearch...
2.0.0
Dream 2.0.0 Release Notes
Release Date: November 23, 2025
Dream 2.0.0 is a major release that rewrites the router using a radix trie data structure, delivering dramatic performance improvements while maintaining Dream's ergonomic API. This release also simplifies the developer experience by introducing EmptyContext and EmptyServices as defaults.
🚨 Breaking Changes
Router API Change
The router constant has been changed to a function router() to support the new radix trie implementation.
Migration:
// Before (v1.x)
import dream/router.{router}
let app_router = router |> route(...)
// After (v2.0)
import dream/router.{router}
let app_router = router() |> route(...)Why: The radix trie requires runtime initialization with dict.new(), which cannot be used in Gleam constants. This breaking change was necessary to achieve O(path depth) performance.
Default Context and Services
server.new() now defaults to EmptyContext and EmptyServices instead of requiring AppContext.
Migration (if you don't use custom context/services):
// Before (v1.x)
import dream/context.{AppContext}
server.new()
|> context(AppContext(request_id: "..."))
|> services(EmptyServices)
|> router(app_router)
// After (v2.0) - Much simpler!
server.new()
|> router(app_router)
|> listen(3000)Migration (if you DO use custom context/services):
// Before (v1.x)
import dream/context.{AppContext}
server.new()
|> context(AppContext(request_id: "..."))
|> router(app_router)
// After (v2.0) - Explicit context() call still works
import dream/context.{type AppContext, new_context}
server.new()
|> context(new_context("req-123"))
|> router(app_router)Why: Most applications don't need per-request context. This change reduces boilerplate and makes simple applications even simpler.
Performance Improvements
Radix Trie Router
The router has been completely rewritten using a radix trie data structure, replacing the previous linear search with O(path depth) lookup.
Benchmark Results (100 routes):
| Scenario | Before (Linear) | After (Radix Trie) | Improvement |
|---|---|---|---|
| First route match | 135.0μs | 0.56μs | 241x faster |
| Middle route match | 70.0μs | 1.44μs | 49x faster |
| Last route match | 7.3μs | 1.35μs | 5.4x faster |
| Route not found | 2500.0μs | 1.85μs | 1353x faster |
Scalability:
The radix trie delivers consistent ~1.3-1.5μs lookup times regardless of route count. Whether you have 100 routes or 1000 routes, performance remains constant. The router is now fast enough that it will never be your bottleneck.
New Features
EmptyContext Type
Added EmptyContext for applications that don't require per-request context:
import dream/context.{type EmptyContext}
import dream/servers/mist/server
pub fn main() {
server.new() // Defaults to EmptyContext and EmptyServices
|> router(my_router())
|> listen(3000)
}Extension Stripping for Content Negotiation
Routes without explicit extensions now match paths with extensions, enabling controllers to handle format detection:
// Route definition
router()
|> route(method: Get, path: "/products/:id", controller: show_product, middleware: [])
// Matches both:
// - /products/1 → id = "1", format = None
// - /products/1.json → id = "1.json", format = Some("json")
fn show_product(request: Request, context: Context, services: Services) -> Response {
use id_param <- result.try(require_string(request, "id"))
case id_param.format {
Some("json") -> json_response(ok, product_data)
Some("csv") -> csv_response(ok, product_data)
_ -> html_response(ok, product_view)
}
}Explicit extension patterns (e.g., *.{jpg,png}) take precedence over extension stripping.
Parameter Name Remapping
Routes can now use different parameter names at the same position without conflict:
router()
|> route(method: Get, path: "/users/:id", controller: show_user, middleware: [])
|> route(method: Get, path: "/users/:user_id/posts", controller: user_posts, middleware: [])
// Both routes work correctly:
// - /users/123 → extracts "id=123"
// - /users/456/posts → extracts "user_id=456"The router preserves each route's original parameter names and remaps them after lookup.
Code Quality Improvements
Eliminated Nested Cases
All nested case statements have been removed across the codebase:
dream/http/transaction: Refactoredget_headerandget_cookieto use recursive helpersdream/router/trie: Refactoredlookup_in_nodeto extract nested logic- Test helper functions refactored to use flat case structures
Removed Anonymous Functions
All anonymous functions have been extracted to named helpers:
dream/servers/mist/handler:create_request_handlerdream/router:apply_middleware_to_controllerdream/http/transaction:header_name_mismatch,cookie_name_mismatch
Fixed All Compiler Warnings
- Removed unused imports across all modules
- Fixed redundant tuples
- Removed unused function arguments
- Added proper underscores for intentionally unused parameters
New Modules
dream/router/trie
Complete radix trie implementation with comprehensive inline documentation:
new()- Create a new empty trieinsert()- Insert a route into the trielookup()- Look up a path and extract parameters- Supports all Dream routing features: literals, parameters, wildcards, multi-wildcards, extension patterns
dream/router/parser
Path pattern parser for converting string paths to structured segments:
parse_path()- Parse a path string into segmentsSegmenttype:Literal,Param,SingleWildcard,MultiWildcard,ExtensionPattern
Documentation Updates
Performance Information
All router documentation now explains radix trie performance:
- O(path depth) lookup complexity
- Consistent ~1.3-1.5μs performance regardless of route count
- Benchmarks showing 100-1000x improvements for common scenarios
Type Safety Trade-off
Documentation now clearly explains Dream's design decision:
- ✅ Controllers ARE type-checked (context, services types verified by compiler)
- ❌ Path parameters are validated at runtime, not compile-time
- This favors ergonomic APIs over compile-time guarantees
- Links to Discussion #15 for type-safe routing exploration
Updated Examples
- All 8 example applications updated to use
router()function - Examples simplified by removing unnecessary
context()andservices()calls - Controllers and middleware use full parameter names (
request,context,services) - README example includes comprehensive comments explaining middleware and controller patterns
Bug Fixes
- Multi-wildcard routes now correctly match zero segments (e.g.,
/public/matches/public/**filepath) - Extension stripping preserves original parameter values for format detection
- Parameter name consistency when routes share parameter positions
- Database example integration tests (parameter flow fixed)
- Multi-format example PostgreSQL connection (port mismatch resolved)
Internal Architecture Changes
Centralized Route Execution
Added dream.execute_route() to centralize route execution logic:
- Handler now passes extracted parameters directly to
execute_route() - Avoids redundant
find_route()calls - Improved performance and code clarity
Removed Deprecated Functions
The following deprecated functions have been removed:
router.method()- Use labeled arguments inroute()insteadrouter.path()- Use labeled arguments inroute()insteadrouter.controller()- Use labeled arguments inroute()insteadrouter.middleware()- Use labeled arguments inroute()insteadrouter.match_path()- Usefind_route()instead
Upgrading
Update your dependencies to 2.0.0:
[dependencies]
dream = ">= 2.0.0 and < 3.0.0"Then run:
gleam deps downloadMigration Checklist
- ✅ Change all
routerreferences torouter()in your code - ✅ Remove unnecessary
context()andservices()calls (if not using custom context/services) - ✅ If using
AppContext, import it explicitly:import dream/context.{type AppContext, new_context} - ✅ Run
gleam testto verify all tests pass - ✅ Run your integration tests to ensure routing still works correctly
Testing
This release includes comprehensive test coverage:
- 62+ router unit tests covering all pattern types, parameter remapping, extension stripping, and edge cases
- Performance benchmark tests comparing radix trie vs linear router
- Integration tests for all 8 example applications
- All tests pass on CI with PostgreSQL integration
Documentation
All packages are available with updated documentation on HexDocs:
Community
Acknowledgments
Special thanks to Louis Pilfold for suggesting the radix trie approach that made th...
1.0.2
Dream 1.0.2 Release Notes
Release Date: November 21, 2025
Dream 1.0.2 is a documentation-focused release that significantly improves the README and adds new concept documentation to make Dream more accessible to newcomers.
What's Changed
README Refactor
The README has been completely rewritten to be more accessible and scannable:
- Reduced from 520 lines to ~130 lines - Much easier to scan and understand
- Zero-knowledge friendly - Explains what Dream, Gleam, and BEAM are for complete beginners
- Complete working example - Shows a full server with line-by-line explanations
- Better structure - Quick introduction with links to detailed docs
Before: Assumed readers knew what Dream, Gleam, and BEAM were, showed type signatures instead of working examples, and was too dense to scan quickly.
After: Starts from zero knowledge, shows working code with explanations, and points to detailed docs for deeper dives.
New Documentation
Added standalone documentation files for better organization:
- How It Works - Detailed explanation of request flow from arrival to response
- Project Structure - How to organize a real Dream application
- Core Patterns - Operations and multi-format response patterns
- Why the BEAM? - Deep dive into the BEAM runtime and its benefits
All detailed technical content that was in the README has been moved to these dedicated files, making the README a quick introduction that points to the right docs.
Module Documentation Updates
All module README files now include HexDocs documentation badges for quick access to API documentation:
dream_configdream_etsdream_http_clientdream_jsondream_opensearchdream_postgres
Terminology Fixes
Clarified the distinction between:
- Controller - A module/file containing multiple actions (e.g.,
controllers/users_controller.gleam) - Controller Action - A function within that controller that handles a request (e.g.,
index,show)
This distinction is now consistently used throughout the documentation.
Upgrading
Update your dependencies to 1.0.2:
[dependencies]
dream = ">= 1.0.2 and < 2.0.0"
dream_config = ">= 1.0.2 and < 2.0.0"
dream_ets = ">= 1.0.2 and < 2.0.0"
dream_http_client = ">= 1.0.2 and < 2.0.0"
dream_json = ">= 1.0.2 and < 2.0.0"
dream_opensearch = ">= 1.0.2 and < 2.0.0"
dream_postgres = ">= 1.0.2 and < 2.0.0"Then run:
gleam deps downloadNote: This is a documentation-only release. No code changes were made. Upgrading is optional but recommended for access to improved documentation.
No Breaking Changes
This release contains no breaking changes or code modifications. All functionality remains identical to 1.0.1.
Documentation
All packages are available with updated documentation on HexDocs:
Full Changelog: CHANGELOG.md
1.0.1
Dream 1.0.1 Release Notes
Release Date: November 22, 2025
Dream 1.0.1 is a patch release that fixes logo display issues on Hex.pm for all Dream packages. This ensures proper branding and visual consistency when viewing package documentation on Hex.pm.
What's Fixed
Logo Display on Hex.pm
Fixed broken logo images on Hex.pm package pages by updating README files to use full GitHub URLs instead of relative paths.
Changed in all packages:
- Main
dreampackage dream_configdream_etsdream_http_clientdream_jsondream_opensearchdream_postgres
Technical Details:
- Updated logo image source from relative path (
ricky_and_lucy.png) to full GitHub URL (https://raw.githubusercontent.com/TrustBound/dream/main/ricky_and_lucy.png) - Added Dream logo to all module README files for consistent branding across the ecosystem
- Logos now display correctly on Hex.pm, GitHub, and any other platform rendering the README
Upgrading
Update your dependencies to 1.0.1:
[dependencies]
dream = ">= 1.0.1 and < 2.0.0"
dream_config = ">= 1.0.1 and < 2.0.0"
dream_ets = ">= 1.0.1 and < 2.0.0"
dream_http_client = ">= 1.0.1 and < 2.0.0"
dream_json = ">= 1.0.1 and < 2.0.0"
dream_opensearch = ">= 1.0.1 and < 2.0.0"
dream_postgres = ">= 1.0.1 and < 2.0.0"Then run:
gleam deps downloadNote: This is a documentation-only release. No code changes were made. Upgrading is optional but recommended for consistency with published documentation.
No Breaking Changes
This release contains no breaking changes or code modifications. All functionality remains identical to 1.0.0.
Documentation
All packages are available with updated documentation on HexDocs:
Full Changelog: CHANGELOG.md