diff --git a/CLAUDE.md b/CLAUDE.md index 3e96746..abaf57f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,6 +13,8 @@ go vet ./... # static analysis ## Architecture - **Module**: `git.codelab.vc/pkg/httpx`, Go 1.24, zero external dependencies + +### Client - **Core pattern**: middleware is `func(http.RoundTripper) http.RoundTripper` - **Chain assembly order** (client.go): Logging → User MW → Retry → CB → Balancer → Transport - Retry wraps CB+Balancer so each attempt can hit a different endpoint @@ -21,10 +23,20 @@ go vet ./... # static analysis - **balancer.Transport** returns `(Middleware, *Closer)` — Closer must be tracked for health checker shutdown - **Client.Close()** stops the health checker goroutine +### Server (`server/`) +- **Core pattern**: middleware is `func(http.Handler) http.Handler` +- **Server** wraps `http.Server` with `net.Listener`, graceful shutdown via signal handling, lifecycle hooks +- **Router** wraps `http.ServeMux` — supports groups with prefix + middleware inheritance, `Mount` for sub-handlers +- **Middleware chain** via `Chain(A, B, C)` — A outermost, C innermost (same as client side) +- **statusWriter** wraps `http.ResponseWriter` to capture status; implements `Unwrap()` for `http.ResponseController` +- **Defaults()** preset: RequestID → Recovery → Logging + production timeouts +- **HealthHandler** exposes `GET /healthz` (liveness) and `GET /readyz` (readiness with pluggable checkers) + ## Conventions -- Functional options for all configuration -- Test helpers: `mockTransport(fn)` wrapping `middleware.RoundTripperFunc` +- Functional options for all configuration (client and server) +- Test helpers: `mockTransport(fn)` wrapping `middleware.RoundTripperFunc` (client), `httptest.NewRecorder`/`httptest.NewRequest` (server) +- Server tests use `waitForAddr(t, srv)` helper to poll until server is ready - No external test frameworks — stdlib only - Thread safety required (`sync.Mutex`/`atomic`) - `internal/clock` for deterministic time testing diff --git a/README.md b/README.md index 733bd67..6a22c5e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # httpx -HTTP client for Go microservices. Retry, load balancing, circuit breaking, all as `http.RoundTripper` middleware. stdlib only, zero external deps. +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 @@ -33,7 +33,9 @@ resp.JSON(&user) ## Packages -Everything is a `func(http.RoundTripper) http.RoundTripper`. Use them with `httpx.Client` or plug into a plain `http.Client`. +### 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 | |---------|-------------| @@ -42,6 +44,20 @@ Everything is a `func(http.RoundTripper) http.RoundTripper`. Use them with `http | `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: ``` @@ -90,6 +106,29 @@ httpClient := &http.Client{ } ``` +## Server + +```go +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.