1b322c8c81c18e0595d71a3ec39eaf6b6af11467
Wraps request body with http.MaxBytesReader to limit incoming payload size. Without this, any endpoint accepting a body is vulnerable to large uploads consuming all available memory. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
httpx
HTTP client and server toolkit for Go microservices. Client side: retry, load balancing, circuit breaking — all as http.RoundTripper middleware. Server side: routing, middleware (request ID, recovery, logging), health checks, graceful shutdown. stdlib only, zero external deps.
go get git.codelab.vc/pkg/httpx
Quick start
client := httpx.New(
httpx.WithBaseURL("https://api.example.com"),
httpx.WithTimeout(10*time.Second),
httpx.WithRetry(retry.WithMaxAttempts(3)),
httpx.WithMiddleware(
middleware.UserAgent("my-service/1.0"),
middleware.BearerAuth(func(ctx context.Context) (string, error) {
return os.Getenv("API_TOKEN"), nil
}),
),
)
defer client.Close()
resp, err := client.Get(ctx, "/users/123")
if err != nil {
log.Fatal(err)
}
var user User
resp.JSON(&user)
Packages
Client
Client middleware is func(http.RoundTripper) http.RoundTripper. Use them with httpx.Client or plug into a plain http.Client.
| Package | What it does |
|---|---|
retry |
Exponential/constant backoff, Retry-After support. Idempotent methods only by default. |
balancer |
Round robin, failover, weighted random. Optional background health checks. |
circuitbreaker |
Per-host state machine (closed/open/half-open). Stops hammering dead endpoints. |
middleware |
Logging (slog), default headers, bearer/basic auth, panic recovery. |
Server
Server middleware is func(http.Handler) http.Handler. The server package provides a production-ready HTTP server.
| Component | What it does |
|---|---|
server.Server |
Wraps http.Server with graceful shutdown, signal handling, lifecycle logging. |
server.Router |
Lightweight wrapper around http.ServeMux with groups, prefix routing, sub-router mounting. |
server.RequestID |
Assigns/propagates X-Request-Id (UUID v4 via crypto/rand). |
server.Recovery |
Recovers panics, returns 500, logs stack trace. |
server.Logging |
Structured request logging (method, path, status, duration, request ID). |
server.HealthHandler |
Liveness (/healthz) and readiness (/readyz) endpoints with pluggable checkers. |
server.Defaults |
Production preset: RequestID → Recovery → Logging + sensible timeouts. |
The client assembles them in this order:
Request → Logging → Your Middleware → Retry → Circuit Breaker → Balancer → Transport
Retry wraps the circuit breaker and balancer, so each attempt can pick a different endpoint.
Multi-DC setup
client := httpx.New(
httpx.WithEndpoints(
balancer.Endpoint{URL: "https://dc1.api.internal", Weight: 3},
balancer.Endpoint{URL: "https://dc2.api.internal", Weight: 1},
),
httpx.WithBalancer(balancer.WithStrategy(balancer.WeightedRandom())),
httpx.WithRetry(retry.WithMaxAttempts(4)),
httpx.WithCircuitBreaker(circuitbreaker.WithFailureThreshold(5)),
httpx.WithLogger(slog.Default()),
)
defer client.Close()
Standalone usage
Each component works with any http.Client, no need for the full wrapper:
// Just retry, nothing else
transport := retry.Transport(retry.WithMaxAttempts(3))
httpClient := &http.Client{
Transport: transport(http.DefaultTransport),
}
// Chain a few middlewares together
chain := middleware.Chain(
middleware.Logging(slog.Default()),
middleware.UserAgent("my-service/1.0"),
retry.Transport(retry.WithMaxAttempts(2)),
)
httpClient := &http.Client{
Transport: chain(http.DefaultTransport),
}
Server
logger := slog.Default()
r := server.NewRouter()
r.HandleFunc("GET /hello", func(w http.ResponseWriter, _ *http.Request) {
w.Write([]byte("world"))
})
// Groups with middleware
api := r.Group("/api/v1", authMiddleware)
api.HandleFunc("GET /users/{id}", getUser)
// Health checks
r.Mount("/", server.HealthHandler(
func() error { return db.Ping() },
))
srv := server.New(r, server.Defaults(logger)...)
log.Fatal(srv.ListenAndServe()) // graceful shutdown on SIGINT/SIGTERM
Requirements
Go 1.24+, stdlib only.
Languages
Go
100%