# httpx HTTP client and server toolkit for Go microservices. Client side: retry, load balancing, circuit breaking, request ID propagation, response size limits — all as `http.RoundTripper` middleware. Server side: routing, middleware (request ID, recovery, logging, CORS, rate limiting, body limits, timeouts), health checks, JSON helpers, graceful shutdown. stdlib only, zero external deps. ``` go get git.codelab.vc/pkg/httpx ``` ## Quick start ```go 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) // PATCH request resp, err = client.Patch(ctx, "/users/123", strings.NewReader(`{"name":"updated"}`)) // Form-encoded request (OAuth, webhooks, etc.) req, _ := httpx.NewFormRequest(ctx, http.MethodPost, "/oauth/token", url.Values{ "grant_type": {"client_credentials"}, "scope": {"read write"}, }) resp, err = client.Do(ctx, req) ``` ## 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, request ID propagation. | ### 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.CORS` | Cross-origin resource sharing with preflight handling and functional options. | | `server.RateLimit` | Per-key token bucket rate limiting with IP extraction and `Retry-After`. | | `server.MaxBodySize` | Limits request body size via `http.MaxBytesReader`. | | `server.Timeout` | Context-based request timeout, returns 503 on expiry. | | `server.WriteJSON` | JSON response helper, sets Content-Type and status. | | `server.WriteError` | JSON error response (`{"error": "..."}`) helper. | | `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 ```go 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: ```go // Just retry, nothing else transport := retry.Transport(retry.WithMaxAttempts(3)) httpClient := &http.Client{ Transport: transport(http.DefaultTransport), } ``` ```go // 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 ```go logger := slog.Default() r := server.NewRouter( // Custom JSON 404 instead of plain text server.WithNotFoundHandler(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { server.WriteError(w, 404, "not found") })), ) r.HandleFunc("GET /hello", func(w http.ResponseWriter, r *http.Request) { server.WriteJSON(w, 200, map[string]string{"message": "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, append(server.Defaults(logger), // Protection middleware server.WithMiddleware( server.CORS( server.AllowOrigins("https://app.example.com"), server.AllowMethods("GET", "POST", "PUT", "PATCH", "DELETE"), server.AllowHeaders("Authorization", "Content-Type"), server.MaxAge(3600), ), server.RateLimit( server.WithRate(100), server.WithBurst(200), ), server.MaxBodySize(1<<20), // 1 MB server.Timeout(30*time.Second), ), )..., ) log.Fatal(srv.ListenAndServe()) // graceful shutdown on SIGINT/SIGTERM ``` ## Client request ID propagation In microservices, forward the incoming request ID to downstream calls: ```go client := httpx.New( httpx.WithMiddleware(middleware.RequestID()), ) // In a server handler — the context already has the request ID from server.RequestID(): func handler(w http.ResponseWriter, r *http.Request) { // ID is automatically forwarded as X-Request-Id resp, err := client.Get(r.Context(), "https://downstream/api") } ``` ## Response body limit Protect against OOM from unexpectedly large upstream responses: ```go client := httpx.New( httpx.WithMaxResponseBody(10 << 20), // 10 MB max ) ``` ## Examples See the [`examples/`](examples/) directory for runnable programs: | Example | Description | |---------|-------------| | [`basic-client`](examples/basic-client/) | HTTP client with retry, timeout, logging, and response size limit | | [`form-request`](examples/form-request/) | Form-encoded POST requests (OAuth, webhooks) | | [`load-balancing`](examples/load-balancing/) | Multi-endpoint client with weighted balancing, circuit breaker, and health checks | | [`server-basic`](examples/server-basic/) | Server with routing, groups, JSON helpers, health checks, and custom 404 | | [`server-protected`](examples/server-protected/) | Production server with CORS, rate limiting, body limits, and timeouts | | [`request-id-propagation`](examples/request-id-propagation/) | Request ID forwarding between server and client for distributed tracing | ## Requirements Go 1.24+, stdlib only.