Aleksey Shakhmatov 85cdc5e2c9
Some checks failed
CI / test (push) Successful in 32s
Publish / publish (push) Failing after 37s
Add publish workflow for Gitea Go Package Registry
Publishes the module to Gitea Package Registry on tag push (v*).
Runs vet and tests before publishing to prevent broken releases.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 10:25:19 +03:00
2026-03-20 10:35:38 +00:00
2026-03-22 22:20:42 +03:00
2026-03-20 10:35:38 +00:00

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

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

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(
    // 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:

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:

client := httpx.New(
    httpx.WithMaxResponseBody(10 << 20), // 10 MB max
)

Examples

See the examples/ directory for runnable programs:

Example Description
basic-client HTTP client with retry, timeout, logging, and response size limit
form-request Form-encoded POST requests (OAuth, webhooks)
load-balancing Multi-endpoint client with weighted balancing, circuit breaker, and health checks
server-basic Server with routing, groups, JSON helpers, health checks, and custom 404
server-protected Production server with CORS, rate limiting, body limits, and timeouts
request-id-propagation Request ID forwarding between server and client for distributed tracing

Requirements

Go 1.24+, stdlib only.

Description
No description provided
Readme MIT 174 KiB
Languages
Go 100%