Http-tower-hs — A Rust Tower-inspired middleware library for Haskell
I’ve been building http-tower-hs, a composable HTTP client middleware library for Haskell, inspired by Rust’s Tower.
The problem
Haskell has solid HTTP clients (http-client, http-client-tls), but no middleware composition story. Every project ends up hand-rolling retry logic, timeout handling, logging, and circuit breakers around raw HTTP calls. Mercury’s recent blog post “A Couple Million Lines of Haskell” described this exact gap — they write all their own HTTP client bindings because there’s no way to inject cross-cutting concerns like tracing, retries, or circuit breakers.
The approach
Two core types:
newtype Service req res = Service
{ runService :: req -> IO (Either ServiceError res) }
type Middleware req res = Service req res -> Service req res
A Service is a function from request to response. Middleware wraps a service to add behavior. You compose them with the |> operator:
client <- newClient
let configured = client
|> withBearerAuth "my-token"
|> withRequestId
|> withRetry (exponentialBackoff 3 0.5 2.0)
|> withTimeout 5000
|> withCircuitBreaker config breaker
|> withValidateStatus (\c -> c >= 200 && c < 300)
|> withTracing
result <- runRequest configured request
case result of
Left err -> handleError err
Right resp -> handleSuccess resp
All errors are Either ServiceError Response — no exceptions escape the stack.
Middleware included (12 so far)
| Middleware | Description |
|---|---|
| Retry | Constant or exponential backoff |
| Timeout | Per-request millisecond timeouts |
| Logging | Pluggable request/response logging |
| Circuit Breaker | Three-state (Closed/Open/HalfOpen) via STM |
| OpenTelemetry Tracing | Automatic spans with stable HTTP semantic conventions |
| Set Header | Add headers, Bearer auth, User-Agent |
| Request ID | UUID v4 correlation IDs |
| Follow Redirect | Automatic 3xx following |
| Filter | Predicate-based request filtering |
| Hedge | Speculative retry via async/race |
| Validate | Status code, Content-Type, header validation |
| Test Double | Mock services, route-based mocks, request recorder |
Design decisions
- Simple function composition over type-level lists for v1 — middleware is just
Service -> Service Eithererrors, not exceptions —ServiceErrorcovers HTTP errors, timeouts, retry exhaustion, circuit breaker open- OpenTelemetry via
hs-opentelemetry-api— no-ops when no TracerProvider is configured, zero overhead for users who don’t use tracing - Generic where possible —
Filter,Hedge,Retry,Timeoutwork with anyreq/res, not just HTTP
Testing
69 tests including property-based tests (QuickCheck) and a Jaeger Docker integration test via testcontainers-haskell that verifies real OTLP span export end-to-end.
Status
This is an early release (0.1.0.0). I’m working on getting it published on Hackage. The source is at github.com/jarlah/http-tower-hs.
I’d appreciate feedback on the API design, especially:
- Is
Service req resthe right core abstraction, or should it be more constrained? - Should middleware ordering be enforced at the type level in a future version?
- Any middleware you’d want to see that’s missing?
Discussion in the ATmosphere