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
  • Either errors, not exceptionsServiceError covers 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 possibleFilter, Hedge, Retry, Timeout work with any req/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 res the 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?
20 Likes

Quick update: http-tower-hs is now published on Hackage hackage.haskell.org/package/http-tower-hs.

CI runs against GHC 9.6.6, 9.8.4, and 9.10.1 (both cabal and stack), so it should work across recent compiler versions. Workflow here if you’re curious: .github/workflows/ci.yml.

Still very much open to feedback on the API design.

1 Like

Also wanted to share a real-world usage example: sentinel, a small infrastructure health monitor built on top of http-tower-hs.

Each probe builds its own middleware stack from config (timeout, retry, circuit breaker, redirect following, header injection, OpenTelemetry tracing), only enabling what’s configured. The README has a full middleware ordering sectionshowing how the code maps to the YAML config.

It was a useful real-world test of the API. If anything felt awkward, it showed up quickly.

1 Like

Very cool!

If you are a Servant user, you can do this using ClientMiddleware, although you need to write the logic yourself

3 Likes

Ill propably convert the library to multiple repos. For ex tower-hs, http-tower-hs and servant-tower-hs. This is also not far from how tower does it in rust.

But thanks for the gentle nudge over the edge :sweat_smile:

something like this @LaurentRDC ?

import Servant.Tower.Adapter
import Tower.Middleware.Retry
import Tower.Middleware.Timeout
import Tower.Middleware.CircuitBreaker

breaker <- newCircuitBreaker
let config = CircuitBreakerConfig { cbFailureThreshold = 5, cbCooldownPeriod = 30 }
    env = withTowerMiddleware
      ( withRetry (exponentialBackoff 3 0.5 2.0)
      . withTimeout 5000
      . withCircuitBreaker config breaker
      ) (mkClientEnv manager baseUrl)
result <- runClientM (getUsers <|> getHealth) env

working on it. Trying to see if it makes sense. I use an adapter approach now. Where http-tower-hs holds the main implementation and servant-tower-hs is just a clean and simple adapter. The question begs, will this work for other libraries as well? is it generic enough? with the current change I’m also able to do generic implementation like this (which sort of answers my question):

import Tower

-- Wrap a database query as a Service
let dbService :: Service SQL.Query [SQL.Row]
    dbService = Service $ \query -> do
      result <- try $ SQL.query conn query
      pure $ case result of
        Left  err  -> Left (TransportError err)
        Right rows -> Right rows

-- Add resilience with the same middleware you'd use for HTTP
breaker <- newCircuitBreaker
let config = CircuitBreakerConfig { cbFailureThreshold = 5, cbCooldownPeriod = 30 }
    robust = withRetry (exponentialBackoff 3 0.5 2.0)
           . withTimeout 5000
           . withCircuitBreaker config breaker
           $ dbService

result <- runService robust "SELECT * FROM users"

which sort of should give the “aha” towards whether this is tied into http directly. The answer is no.

EDIT: working on it now. Making it a mono repo and trying to make as much of the middlewares as generic if possible.

EDIT2: have a PR up here if anyone is interested in looking at it. tried to make it as resusable as possible, and extensible, so it should be possible to use generically with anything not just http, and to implement more packages in the future for ex grpc etc. Refactor into multi-package mono-repo by sillychipmunk · Pull Request #7 · jarlah/http-tower-hs · GitHub

2 Likes

Following up on the monorepo refactoring I mentioned, the library is now published as three separate packages on Hackage:

  • tower-hs — The generic Service/Middleware core. Protocol-agnostic middleware (retry, timeout, circuit breaker, hedge, filter,
    tracing, logging, etc.). Not tied to HTTP at all, works with any req → IO (Either ServiceError res) service.
  • http-tower-hs — HTTP client middleware built on tower-hs (bearer auth, headers, redirects, request IDs, status validation,
    OTel tracing).
  • servant-tower-hs — Servant ClientMiddleware adapter, so you can use the full tower-hs middleware stack with Servant clients,
    plus servant-specific middleware.

The idea is that tower-hs is a standalone foundation you can use to wrap anything, a database client, a gRPC stub, a message queue, with the same resilience patterns. The HTTP and Servant packages layer on top with transport-specific concerns.

You can depend on just the packages you need, e.g. only tower-hs if you’re wrapping non-HTTP services.

Repo: GitHub - jarlah/tower-hs · GitHub

4 Likes

Very interesting. I’ll keep an eye on this as a way to factor out common patterns from service bindings. It’s frustrating that many service bindings implement retries/logging/etc in an ad-hoc way and a good common idiom for that would help.

By the way, (&) is the conventional name for the (|>) operator. I’ve written an earlier explanation about why, but it came first and has much better consistency with the rest of the visual language of Haskell operators.

2 Likes

(&)is also in base :grin:

1 Like

Cool. Thanks. I will definitely add in (&). I could even keep (|>) or phase it out.

Personally i have no strong preferences. Been doing some Elm and other languages that also have used the shove operator but thats the problem right there. Shove. You need a mental picture.

1 Like

I don’t think the (|>) question has a good answer at this point. It does smooth the on-ramp for people coming from pretty much any other FP language. I think defining it with a custom warning group is probably the best compromise position I’ve found so far, because then people coming to Haskell from other languages will search for it by reflex, find it, and then learn what we actually call the operator. But I don’t know who can best carry that responsibility, since it is redundant to have it fall to each package implementer.

P.S.: (&) is defined in base already, in module Data.Function.

Ah thanks for the double hints. Ok so there is shove style composition already in ghc with (&)? I will try it out. Maybe change README.md and test examples to use it.

Here’s some rewrites of your example from OP, edited:

import Data.Functor ((<&>)) -- (<&>) = flip (<$>), like how (&) = flip ($)

client <- newClient
      <&> withBearerAuth "my-token"
      . withRequestId
      . withRetry (exponentialBackoff 3 0.5 2.0)
      . withTimeout 5000
      . withCircuitBreaker config breaker
      . withValidateStatus (\c -> c >= 200 && c < 300)
      . withTracing
import Data.Function ((&))

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

By the way, it might also be useful to give your Service type a Functor instance (can be derived automatically) and a Profunctor instance. If someone was writing a binding to a remote HTTP API, it’d provide a good way to lift a Service Http.Request Http.Response into a Service MyBinding.SomeRequest MyBinding.SomeResponse.

3 Likes

I have update all three libraries with exclusive use of (&) but still left (|>) in there for those who would reach for it.

Thanks for the valuable feedback @jackdk :flexed_biceps:

1 Like

That sounds really cool :slight_smile: thanks! How about making your Service type a Category and an Arrow so they are composable?

3 Likes

added implementations for them. not released yet, just playing with the concept. If you know a better way to describe the section in the readme for the changes, feel free to help me :slight_smile: the implementations isn’t hard to add, but need to be correct, so I added test to verify the laws are followed.

Description in README.md is extremely generic atm. We need to find good use cases with the library

I might revert it if it doesn’t make sense :slight_smile: But its interesting to see where we hit the limits of valid use cases in tower-hs

@turion this is definitely a fantastic learning path/curve for myself too :smiley: so I’m having a blast at digging into whether this can make sense.

I updated README.md: GitHub - jarlah/tower-hs · GitHub

Feedback is highly appreciated :folded_hands:

Toying more with the idea of defining composite semi complex pipelines

Looking at it however i need someone with actual experience with this to see if there is a valid use case for it. It allows great freedom for the use of the Service. But for what ? Cant just add stuff because its nice and shiny :rofl:

My own conclusion at this point is that its not adding complexity to the package. But it might not be remotely useful unless in very specific corner cases.

It only adds value, its fine. I toned down README a bit. Will release tomorrow :slight_smile: Im happy to make the library as versatile as possible without making it solve everything at once. Still open to suggestions :slight_smile: