Files
httpx/AGENTS.md

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

  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
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_<name>.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
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

  1. Add field to the options struct (clientOptions or serverOptions)
  2. Create With<Name> 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

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