dream_http_client
Type-safe HTTP client for Gleam with streaming support.
A standalone HTTP/HTTPS client built on Erlang’s battle-tested httpc. Supports blocking requests, yielder streaming, and message-based streaming. Built with the same quality standards as Dream, but completely independent—use it in any Gleam project.
Contents
- Why dream_http_client?
- Installation
- Quick Start
- Execution Modes
- Recording & Playback
- API Reference
- Examples
Why dream_http_client?
| Feature | What you get |
|---|---|
| Three execution modes | Blocking, yielder streaming, message-based—choose what fits |
| OTP-first design | Message-based streams integrate with actors and selectors |
| Recording/playback | Record HTTP calls for tests, debug production, work offline |
| Type-safe | Result types force error handling—no silent failures |
| Battle-tested | Built on Erlang’s httpc—proven in production for decades |
| Framework-independent | Zero dependencies on Dream or other frameworks |
| Concurrent streams | Handle multiple HTTP streams in a single actor |
| Stream cancellation | Cancel in-flight requests cleanly |
| Builder pattern | Consistent, composable request configuration |
Installation
gleam add dream_http_client
Quick Start
Make a simple HTTP request:
import dream_http_client/client
import gleam/http
pub fn fetch_data() {
client.new
|> client.method(http.Get)
|> client.scheme(http.Https)
|> client.host("api.example.com")
|> client.path("/users/123")
|> client.add_header("Authorization", "Bearer " <> token)
|> client.send()
}
Execution Modes
dream_http_client provides three execution modes. Choose based on your use case:
1. Blocking - send()
Best for: JSON APIs, small responses
let result = client.new
|> client.host("api.example.com")
|> client.path("/users")
|> client.send()
case result {
Ok(body) -> decode_json(body)
Error(msg) -> handle_error(msg)
}
2. Yielder Streaming - stream_yielder()
Best for: AI/LLM streaming, file downloads, sequential processing
import gleam/yielder
client.new
|> client.host("api.openai.com")
|> client.path("/v1/chat/completions")
|> client.stream_yielder()
|> yielder.each(fn(chunk_result) {
case chunk_result {
Ok(chunk) -> process_chunk(chunk)
Error(reason) -> log_error(reason)
}
})
⚠️ Note: This blocks while waiting for chunks. Not suitable for OTP actors handling concurrent operations.
3. Process-Based Streaming - start_stream()
Best for: Background tasks, concurrent operations, cancellable streams
// Start stream with callbacks - returns immediately
let assert Ok(stream) = client.new
|> client.host("api.openai.com")
|> client.path("/v1/chat/completions")
|> client.on_stream_chunk(fn(data) {
case bit_array.to_string(data) {
Ok(text) -> io.print(text)
Error(_) -> Nil
}
})
|> client.start_stream()
// Wait for completion if needed
client.await_stream(stream)
// Or cancel early
client.cancel_stream_handle(stream)
Choosing a Mode
| Use Case | Mode | Why |
|---|---|---|
| JSON API calls | send() | Simple, complete response at once |
| Small file downloads | send() | Load entire file into memory |
| AI/LLM streaming (single request) | stream_yielder() | Sequential token processing |
| File downloads | stream_yielder() | Memory-efficient chunked processing |
| Background processing | start_stream() | Non-blocking, concurrent, cancellable |
| Long-lived connections | start_stream() | Can cancel mid-stream |
| Cancellable operations | start_stream() | Cancel via handle |
Recording & Playback
Record HTTP requests/responses for testing, debugging, and offline development.
Quick Example
import dream_http_client/recorder
import dream_http_client/matching
// Record real requests
let assert Ok(rec) = recorder.start(
recorder.Record(directory: "mocks/api"),
matching.match_url_only(),
)
client.new
|> client.host("api.example.com")
|> client.recorder(rec)
|> client.send() // Saved immediately to disk
// Playback later (no network)
let assert Ok(playback) = recorder.start(
recorder.Playback(directory: "mocks/api"),
matching.match_url_only(),
)
client.new
|> client.host("api.example.com")
|> client.recorder(playback)
|> client.send() // Returns recorded response
Recording Modes
Record(directory)- Records real requests to disk immediatelyPlayback(directory)- Returns recorded responses (no network)Passthrough- No recording/playback
Important: Recordings are saved immediately when captured. recorder.stop() is optional and only performs cleanup. This ensures recordings are never lost even if the process crashes.
Use Cases
Testing:
// test/api_test.gleam
let assert Ok(rec) = recorder.start(
recorder.Playback(directory: "test/fixtures/api"),
matching.match_url_only(),
)
// Tests run without external dependencies
Offline Development: Record API responses once, then work offline using recorded responses.
Debugging Production: Record problematic request/response pairs for investigation.
Request Matching
import dream_http_client/matching
// Default: Match on method + URL only
let config = matching.match_url_only()
// Custom matching
let config = matching.MatchingConfig(
match_method: True,
match_url: True,
match_headers: False, // Ignore auth tokens, timestamps
match_body: False, // Ignore request IDs in body
)
Performance Note
Recording uses a read-modify-write approach (O(n) where n is existing recordings), prioritizing reliability over performance. This is suitable for typical use cases (record once, playback often). If you need high-performance recording, please create an issue at https://github.com/TrustBound/dream/issues.
API Reference
Builder Pattern
client.new
|> client.method(http.Post) // HTTP method
|> client.scheme(http.Https) // HTTP or HTTPS
|> client.host("api.example.com") // Hostname (required)
|> client.port(443) // Port (optional, defaults 80/443)
|> client.path("/api/users") // Request path
|> client.query("page=1&limit=10") // Query string
|> client.add_header("Content-Type", "application/json")
|> client.body(json_body) // Request body
|> client.timeout(60_000) // Timeout in ms (default: 30s)
Execution
Blocking:
send(req) -> Result(String, String)- Returns complete response body
Yielder Streaming:
stream_yielder(req) -> Yielder(Result(BytesTree, String))- Returns yielder producing chunks
Message-Based Streaming:
stream_messages(req) -> Result(RequestId, String)- Starts stream, returns IDselect_stream_messages(selector, mapper) -> Selector(msg)- Integrates with OTP selectorscancel_stream(request_id)- Cancels active stream
Types
RequestId - Opaque identifier for message-based streams
StreamMessage:
StreamStart(request_id, headers)- Stream startedChunk(request_id, data)- Data chunk receivedStreamEnd(request_id, headers)- Stream completedStreamError(request_id, reason)- Stream failedDecodeError(reason)- FFI corruption (report as bug)
Error Handling
All modes use Result types for explicit error handling:
case client.send(request) {
Ok(body) -> process_response(body)
Error(msg) -> {
// Common errors:
// - Connection refused
// - DNS resolution failed
// - Timeout
log_error(msg)
}
}
Examples
All examples are tested and verified. See test/snippets/ for complete, runnable code.
Basic requests:
- Blocking request - Simple GET
- POST with JSON - JSON body
- Request builder - Full configuration
- Timeout configuration - Custom timeouts
Streaming:
- Yielder streaming - Sequential processing
Recording:
- Record and playback - Testing without network
Design Principles
This module follows the same quality standards as Dream:
- No nested cases - Clear, flat control flow throughout
- No anonymous functions - All functions are named for clarity
- Builder pattern - Consistent, composable request configuration
- Type safety -
Resulttypes force error handling at compile time - OTP-first design - Message-based API designed for supervision trees
- Comprehensive testing - Unit tests (no network) + integration tests (real HTTP)
- Battle-tested foundation - Built on Erlang’s production-proven
httpc
About Dream
This module was originally built for the Dream web toolkit, but it’s completely standalone and can be used in any Gleam project. It follows Dream’s design principles and will be maintained as part of the Dream ecosystem.
License
MIT — see LICENSE.md