{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreighfm5f5t7qh3q46n53v2ijxqbk4ybns5gjxcogtzcapolbppyjz4",
"uri": "at://did:plc:pi6woz4d47bkuws673w2il2r/app.bsky.feed.post/3miqsbdanmyn2"
},
"path": "/t/http-tower-hs-a-rust-tower-inspired-middleware-library-for-haskell/13892#post_1",
"publishedAt": "2026-04-05T08:31:58.000Z",
"site": "https://discourse.haskell.org",
"tags": [
"http-tower-hs",
"github.com/jarlah/http-tower-hs"
],
"textContent": "I’ve been building http-tower-hs, a composable HTTP client middleware library for Haskell, inspired by Rust’s Tower.\n\n## The problem\n\nHaskell 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.\n\n## The approach\n\nTwo core types:\n\n\n newtype Service req res = Service\n { runService :: req -> IO (Either ServiceError res) }\n\n type Middleware req res = Service req res -> Service req res\n\n\nA `Service` is a function from request to response. `Middleware` wraps a service to add behavior. You compose them with the `|>` operator:\n\n\n client <- newClient\n let configured = client\n |> withBearerAuth \"my-token\"\n |> withRequestId\n |> withRetry (exponentialBackoff 3 0.5 2.0)\n |> withTimeout 5000\n |> withCircuitBreaker config breaker\n |> withValidateStatus (\\c -> c >= 200 && c < 300)\n |> withTracing\n\n result <- runRequest configured request\n case result of\n Left err -> handleError err\n Right resp -> handleSuccess resp\n\n\nAll errors are `Either ServiceError Response` — no exceptions escape the stack.\n\n## Middleware included (12 so far)\n\nMiddleware | Description\n---|---\n**Retry** | Constant or exponential backoff\n**Timeout** | Per-request millisecond timeouts\n**Logging** | Pluggable request/response logging\n**Circuit Breaker** | Three-state (Closed/Open/HalfOpen) via STM\n**OpenTelemetry Tracing** | Automatic spans with stable HTTP semantic conventions\n**Set Header** | Add headers, Bearer auth, User-Agent\n**Request ID** | UUID v4 correlation IDs\n**Follow Redirect** | Automatic 3xx following\n**Filter** | Predicate-based request filtering\n**Hedge** | Speculative retry via `async`/`race`\n**Validate** | Status code, Content-Type, header validation\n**Test Double** | Mock services, route-based mocks, request recorder\n\n## Design decisions\n\n * **Simple function composition** over type-level lists for v1 — middleware is just `Service -> Service`\n * **`Either` errors, not exceptions** — `ServiceError` covers HTTP errors, timeouts, retry exhaustion, circuit breaker open\n * **OpenTelemetry via`hs-opentelemetry-api`** — no-ops when no TracerProvider is configured, zero overhead for users who don’t use tracing\n * **Generic where possible** — `Filter`, `Hedge`, `Retry`, `Timeout` work with any `req`/`res`, not just HTTP\n\n\n\n## Testing\n\n69 tests including property-based tests (QuickCheck) and a Jaeger Docker integration test via `testcontainers-haskell` that verifies real OTLP span export end-to-end.\n\n## Status\n\nThis 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.\n\nI’d appreciate feedback on the API design, especially:\n\n * Is `Service req res` the right core abstraction, or should it be more constrained?\n * Should middleware ordering be enforced at the type level in a future version?\n * Any middleware you’d want to see that’s missing?\n\n",
"title": "Http-tower-hs — A Rust Tower-inspired middleware library for Haskell"
}