5.1 KiB
5.1 KiB
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
- Create file in
middleware/(or inline) - Return
middleware.Middleware(func(http.RoundTripper) http.RoundTripper) - Use
middleware.RoundTripperFuncfor the inner adapter - Test with
middleware.RoundTripperFuncas mock transport
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
- Create file in
server/namedmiddleware_<name>.go - Return
server.Middleware(func(http.Handler) http.Handler) - Use
server.statusWriterif you need to capture the response status - Test with
httptest.NewRecorder+httptest.NewRequest
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
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
- Add field to the options struct (
clientOptionsorserverOptions) - Create
With<Name>function returningOption - 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/: Bothserverandmiddlewarepackages need request ID context. The shared key lives ininternal/requestid— do NOT importserverfrommiddlewareor 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 setGetBody) or set it manually - statusWriter.Unwrap(): Server middleware must not type-assert
http.ResponseWriterdirectly — usehttp.ResponseControllerwhich callsUnwrap()to findhttp.Flusher,http.Hijacker, etc. - No external deps: This is a zero-dependency library. Do not add any
requiretogo.mod
Commands
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, oratomicwhere needed internal/clock— use for deterministic time in tests (nevertime.Now()directly in testable code)- Test helpers:
mockTransport(fn)wrappingmiddleware.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