49be6f8a7e9c9f740d4e63f31c0ddf24e839bbe7
Introduces internal/requestid package with shared context key to avoid circular imports between server and middleware packages. Server's RequestID middleware now uses the shared key. Client middleware picks up the ID from context and sets X-Request-Id on outgoing requests. 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%