Add AI agent configuration files (AGENTS.md, .cursorrules, copilot-instructions)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
139
AGENTS.md
Normal file
139
AGENTS.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# 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_<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`
|
||||
|
||||
```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<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
|
||||
|
||||
```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
|
||||
Reference in New Issue
Block a user