# AGENTS.md — httpx Universal guide for AI coding agents working with this codebase. ## Overview `git.codelab.vc/pkg/httpx` is a Go HTTP toolkit with **zero external dependencies** (Go 1.24, stdlib only). It provides: - A composable HTTP **client** with retry, circuit breaking, load balancing - A production-ready HTTP **server** with routing, middleware, graceful shutdown ## Package map ``` httpx/ Root — Client, request builders, error types ├── middleware/ Client-side middleware (RoundTripper wrappers) ├── retry/ Retry middleware with backoff ├── circuitbreaker/ Per-host circuit breaker ├── balancer/ Client-side load balancing + health checking ├── server/ Server, Router, server-side middleware, response helpers └── internal/ ├── requestid/ Shared context key (avoids circular imports) └── clock/ Deterministic time for testing ``` ## Middleware chain architecture ### Client middleware: `func(http.RoundTripper) http.RoundTripper` ``` Request flow (outermost → innermost): Logging └→ User Middlewares └→ Retry └→ Circuit Breaker └→ Balancer └→ Base Transport (http.DefaultTransport) ``` Retry wraps CB+Balancer so each attempt can hit a different endpoint. ### Server middleware: `func(http.Handler) http.Handler` ``` Chain(A, B, C)(handler) == A(B(C(handler))) A is outermost (sees request first, response last) ``` Defaults() preset: `RequestID → Recovery → Logging` ## Common tasks ### Add a client middleware 1. Create file in `middleware/` (or inline) 2. Return `middleware.Middleware` (`func(http.RoundTripper) http.RoundTripper`) 3. Use `middleware.RoundTripperFunc` for the inner adapter 4. Test with `middleware.RoundTripperFunc` as mock transport ```go func MyMiddleware() middleware.Middleware { return func(next http.RoundTripper) http.RoundTripper { return middleware.RoundTripperFunc(func(req *http.Request) (*http.Response, error) { // before resp, err := next.RoundTrip(req) // after return resp, err }) } } ``` ### Add a server middleware 1. Create file in `server/` named `middleware_.go` 2. Return `server.Middleware` (`func(http.Handler) http.Handler`) 3. Use `server.statusWriter` if you need to capture the response status 4. Test with `httptest.NewRecorder` + `httptest.NewRequest` ```go func MyMiddleware() Middleware { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // before next.ServeHTTP(w, r) // after }) } } ``` ### Add a route ```go r := server.NewRouter() r.HandleFunc("GET /users/{id}", getUser) r.HandleFunc("POST /users", createUser) // Group with prefix + middleware api := r.Group("/api/v1", authMiddleware) api.HandleFunc("GET /items", listItems) // Mount sub-handler r.Mount("/health", server.HealthHandler()) ``` ### Add a functional option 1. Add field to the options struct (`clientOptions` or `serverOptions`) 2. Create `With` function returning `Option` 3. Apply the field in the constructor (`New`) ## Gotchas - **Middleware order matters**: Retry wraps CB+Balancer intentionally — each retry attempt can hit a different endpoint and a different circuit breaker - **Circular imports via `internal/`**: Both `server` and `middleware` packages need request ID context. The shared key lives in `internal/requestid` — do NOT import `server` from `middleware` or vice versa - **Client.Close() is required** when using `WithEndpoints()` — the balancer starts a background health checker goroutine that must be stopped - **GetBody for retries**: Request bodies must be replayable. Use `NewJSONRequest`/`NewFormRequest` (they set `GetBody`) or set it manually - **statusWriter.Unwrap()**: Server middleware must not type-assert `http.ResponseWriter` directly — use `http.ResponseController` which calls `Unwrap()` to find `http.Flusher`, `http.Hijacker`, etc. - **No external deps**: This is a zero-dependency library. Do not add any `require` to `go.mod` ## Commands ```bash go build ./... # compile go test ./... # all tests go test -race ./... # tests with race detector go test -v -run TestName ./package/ # single test go vet ./... # static analysis ``` ## Conventions - **Functional options** for all configuration (client and server) - **stdlib only** testing — no testify, no gomock - **Thread safety** — use `sync.Mutex`, `sync.Map`, or `atomic` where needed - **`internal/clock`** — use for deterministic time in tests (never `time.Now()` directly in testable code) - **Test helpers**: `mockTransport(fn)` wrapping `middleware.RoundTripperFunc` (client), `httptest.NewRecorder`/`httptest.NewRequest` (server), `waitForAddr(t, srv)` for server integration tests - **Sentinel errors** live in sub-packages, root package re-exports as aliases