Update documentation with new client and server features
All checks were successful
CI / test (push) Successful in 30s

README: add PATCH, NewFormRequest, CORS, RateLimit, MaxBodySize,
Timeout, WriteJSON/WriteError, request ID propagation, response body
limit, and custom 404 handler examples. CLAUDE.md: document new
architecture details for all added components.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-22 21:55:15 +03:00
parent 8a63f142a7
commit 89cfc38f0e
2 changed files with 83 additions and 7 deletions

View File

@@ -22,15 +22,25 @@ go vet ./... # static analysis
- **Sentinel errors**: canonical values live in sub-packages, root package re-exports as aliases - **Sentinel errors**: canonical values live in sub-packages, root package re-exports as aliases
- **balancer.Transport** returns `(Middleware, *Closer)` — Closer must be tracked for health checker shutdown - **balancer.Transport** returns `(Middleware, *Closer)` — Closer must be tracked for health checker shutdown
- **Client.Close()** stops the health checker goroutine - **Client.Close()** stops the health checker goroutine
- **Client.Patch()** — PATCH method, same pattern as Put/Post
- **NewFormRequest** — form-encoded request builder (`application/x-www-form-urlencoded`) with `GetBody` for retry
- **WithMaxResponseBody** — wraps `resp.Body` with `io.LimitedReader` to prevent OOM
- **middleware.RequestID()** — propagates request ID from context to outgoing `X-Request-Id` header
- **`internal/requestid`** — shared context key used by both `server` and `middleware` packages to avoid circular imports
### Server (`server/`) ### Server (`server/`)
- **Core pattern**: middleware is `func(http.Handler) http.Handler` - **Core pattern**: middleware is `func(http.Handler) http.Handler`
- **Server** wraps `http.Server` with `net.Listener`, graceful shutdown via signal handling, lifecycle hooks - **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 - **Router** wraps `http.ServeMux` — supports groups with prefix + middleware inheritance, `Mount` for sub-handlers, `WithNotFoundHandler` for custom 404
- **Middleware chain** via `Chain(A, B, C)` — A outermost, C innermost (same as client side) - **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` - **statusWriter** wraps `http.ResponseWriter` to capture status; implements `Unwrap()` for `http.ResponseController`
- **Defaults()** preset: RequestID → Recovery → Logging + production timeouts - **Defaults()** preset: RequestID → Recovery → Logging + production timeouts
- **HealthHandler** exposes `GET /healthz` (liveness) and `GET /readyz` (readiness with pluggable checkers) - **HealthHandler** exposes `GET /healthz` (liveness) and `GET /readyz` (readiness with pluggable checkers)
- **CORS** middleware — preflight OPTIONS handling, `AllowOrigins`, `AllowMethods`, `AllowHeaders`, `ExposeHeaders`, `AllowCredentials`, `MaxAge`
- **RateLimit** middleware — per-key token bucket (`sync.Map`), IP from `X-Forwarded-For`, `WithRate`/`WithBurst`/`WithKeyFunc`, uses `internal/clock`
- **MaxBodySize** middleware — wraps `r.Body` via `http.MaxBytesReader`
- **Timeout** middleware — wraps `http.TimeoutHandler`, returns 503
- **WriteJSON** / **WriteError** — JSON response helpers in `server/respond.go`
## Conventions ## Conventions

View File

@@ -1,6 +1,6 @@
# httpx # httpx
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. HTTP client and server toolkit for Go microservices. Client side: retry, load balancing, circuit breaking, request ID propagation, response size limits — all as `http.RoundTripper` middleware. Server side: routing, middleware (request ID, recovery, logging, CORS, rate limiting, body limits, timeouts), health checks, JSON helpers, graceful shutdown. stdlib only, zero external deps.
``` ```
go get git.codelab.vc/pkg/httpx go get git.codelab.vc/pkg/httpx
@@ -29,6 +29,16 @@ if err != nil {
var user User var user User
resp.JSON(&user) resp.JSON(&user)
// PATCH request
resp, err = client.Patch(ctx, "/users/123", strings.NewReader(`{"name":"updated"}`))
// Form-encoded request (OAuth, webhooks, etc.)
req, _ := httpx.NewFormRequest(ctx, http.MethodPost, "/oauth/token", url.Values{
"grant_type": {"client_credentials"},
"scope": {"read write"},
})
resp, err = client.Do(ctx, req)
``` ```
## Packages ## Packages
@@ -42,7 +52,7 @@ Client middleware is `func(http.RoundTripper) http.RoundTripper`. Use them with
| `retry` | Exponential/constant backoff, Retry-After support. Idempotent methods only by default. | | `retry` | Exponential/constant backoff, Retry-After support. Idempotent methods only by default. |
| `balancer` | Round robin, failover, weighted random. Optional background health checks. | | `balancer` | Round robin, failover, weighted random. Optional background health checks. |
| `circuitbreaker` | Per-host state machine (closed/open/half-open). Stops hammering dead endpoints. | | `circuitbreaker` | Per-host state machine (closed/open/half-open). Stops hammering dead endpoints. |
| `middleware` | Logging (slog), default headers, bearer/basic auth, panic recovery. | | `middleware` | Logging (slog), default headers, bearer/basic auth, panic recovery, request ID propagation. |
### Server ### Server
@@ -56,6 +66,12 @@ Server middleware is `func(http.Handler) http.Handler`. The `server` package pro
| `server.Recovery` | Recovers panics, returns 500, logs stack trace. | | `server.Recovery` | Recovers panics, returns 500, logs stack trace. |
| `server.Logging` | Structured request logging (method, path, status, duration, request ID). | | `server.Logging` | Structured request logging (method, path, status, duration, request ID). |
| `server.HealthHandler` | Liveness (`/healthz`) and readiness (`/readyz`) endpoints with pluggable checkers. | | `server.HealthHandler` | Liveness (`/healthz`) and readiness (`/readyz`) endpoints with pluggable checkers. |
| `server.CORS` | Cross-origin resource sharing with preflight handling and functional options. |
| `server.RateLimit` | Per-key token bucket rate limiting with IP extraction and `Retry-After`. |
| `server.MaxBodySize` | Limits request body size via `http.MaxBytesReader`. |
| `server.Timeout` | Context-based request timeout, returns 503 on expiry. |
| `server.WriteJSON` | JSON response helper, sets Content-Type and status. |
| `server.WriteError` | JSON error response (`{"error": "..."}`) helper. |
| `server.Defaults` | Production preset: RequestID → Recovery → Logging + sensible timeouts. | | `server.Defaults` | Production preset: RequestID → Recovery → Logging + sensible timeouts. |
The client assembles them in this order: The client assembles them in this order:
@@ -111,9 +127,15 @@ httpClient := &http.Client{
```go ```go
logger := slog.Default() logger := slog.Default()
r := server.NewRouter() r := server.NewRouter(
r.HandleFunc("GET /hello", func(w http.ResponseWriter, _ *http.Request) { // Custom JSON 404 instead of plain text
w.Write([]byte("world")) server.WithNotFoundHandler(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
server.WriteError(w, 404, "not found")
})),
)
r.HandleFunc("GET /hello", func(w http.ResponseWriter, r *http.Request) {
server.WriteJSON(w, 200, map[string]string{"message": "world"})
}) })
// Groups with middleware // Groups with middleware
@@ -125,10 +147,54 @@ r.Mount("/", server.HealthHandler(
func() error { return db.Ping() }, func() error { return db.Ping() },
)) ))
srv := server.New(r, server.Defaults(logger)...) srv := server.New(r,
append(server.Defaults(logger),
// Protection middleware
server.WithMiddleware(
server.CORS(
server.AllowOrigins("https://app.example.com"),
server.AllowMethods("GET", "POST", "PUT", "PATCH", "DELETE"),
server.AllowHeaders("Authorization", "Content-Type"),
server.MaxAge(3600),
),
server.RateLimit(
server.WithRate(100),
server.WithBurst(200),
),
server.MaxBodySize(1<<20), // 1 MB
server.Timeout(30*time.Second),
),
)...,
)
log.Fatal(srv.ListenAndServe()) // graceful shutdown on SIGINT/SIGTERM log.Fatal(srv.ListenAndServe()) // graceful shutdown on SIGINT/SIGTERM
``` ```
## Client request ID propagation
In microservices, forward the incoming request ID to downstream calls:
```go
client := httpx.New(
httpx.WithMiddleware(middleware.RequestID()),
)
// In a server handler — the context already has the request ID from server.RequestID():
func handler(w http.ResponseWriter, r *http.Request) {
// ID is automatically forwarded as X-Request-Id
resp, err := client.Get(r.Context(), "https://downstream/api")
}
```
## Response body limit
Protect against OOM from unexpectedly large upstream responses:
```go
client := httpx.New(
httpx.WithMaxResponseBody(10 << 20), // 10 MB max
)
```
## Requirements ## Requirements
Go 1.24+, stdlib only. Go 1.24+, stdlib only.