Update documentation with new client and server features
All checks were successful
CI / test (push) Successful in 30s
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:
78
README.md
78
README.md
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
@@ -29,6 +29,16 @@ if err != nil {
|
||||
|
||||
var user 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
|
||||
@@ -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. |
|
||||
| `balancer` | Round robin, failover, weighted random. Optional background health checks. |
|
||||
| `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
|
||||
|
||||
@@ -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.Logging` | Structured request logging (method, path, status, duration, request ID). |
|
||||
| `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. |
|
||||
|
||||
The client assembles them in this order:
|
||||
@@ -111,9 +127,15 @@ httpClient := &http.Client{
|
||||
```go
|
||||
logger := slog.Default()
|
||||
|
||||
r := server.NewRouter()
|
||||
r.HandleFunc("GET /hello", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Write([]byte("world"))
|
||||
r := server.NewRouter(
|
||||
// Custom JSON 404 instead of plain text
|
||||
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
|
||||
@@ -125,10 +147,54 @@ r.Mount("/", server.HealthHandler(
|
||||
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
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
Go 1.24+, stdlib only.
|
||||
|
||||
Reference in New Issue
Block a user