Add examples/ with runnable usage demos for all major features
All checks were successful
CI / test (push) Successful in 31s
All checks were successful
CI / test (push) Successful in 31s
Six examples covering the full API surface: - basic-client: retry, timeout, logging, response size limit - form-request: form-encoded POST for OAuth/webhooks - load-balancing: weighted endpoints, circuit breaker, health checks - server-basic: routing, groups, JSON helpers, health, custom 404 - server-protected: CORS, rate limiting, body limits, timeouts - request-id-propagation: cross-service request ID forwarding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
13
README.md
13
README.md
@@ -195,6 +195,19 @@ client := httpx.New(
|
||||
)
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
See the [`examples/`](examples/) directory for runnable programs:
|
||||
|
||||
| Example | Description |
|
||||
|---------|-------------|
|
||||
| [`basic-client`](examples/basic-client/) | HTTP client with retry, timeout, logging, and response size limit |
|
||||
| [`form-request`](examples/form-request/) | Form-encoded POST requests (OAuth, webhooks) |
|
||||
| [`load-balancing`](examples/load-balancing/) | Multi-endpoint client with weighted balancing, circuit breaker, and health checks |
|
||||
| [`server-basic`](examples/server-basic/) | Server with routing, groups, JSON helpers, health checks, and custom 404 |
|
||||
| [`server-protected`](examples/server-protected/) | Production server with CORS, rate limiting, body limits, and timeouts |
|
||||
| [`request-id-propagation`](examples/request-id-propagation/) | Request ID forwarding between server and client for distributed tracing |
|
||||
|
||||
## Requirements
|
||||
|
||||
Go 1.24+, stdlib only.
|
||||
|
||||
67
examples/basic-client/main.go
Normal file
67
examples/basic-client/main.go
Normal file
@@ -0,0 +1,67 @@
|
||||
// Basic HTTP client with retry, timeout, and structured logging.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"git.codelab.vc/pkg/httpx"
|
||||
"git.codelab.vc/pkg/httpx/middleware"
|
||||
"git.codelab.vc/pkg/httpx/retry"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := httpx.New(
|
||||
httpx.WithBaseURL("https://httpbin.org"),
|
||||
httpx.WithTimeout(10*time.Second),
|
||||
httpx.WithRetry(
|
||||
retry.WithMaxAttempts(3),
|
||||
retry.WithBackoff(retry.ExponentialBackoff(100*time.Millisecond, 2*time.Second, true)),
|
||||
),
|
||||
httpx.WithMiddleware(
|
||||
middleware.UserAgent("httpx-example/1.0"),
|
||||
),
|
||||
httpx.WithMaxResponseBody(1<<20), // 1 MB limit
|
||||
httpx.WithLogger(slog.Default()),
|
||||
)
|
||||
defer client.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// GET request.
|
||||
resp, err := client.Get(ctx, "/get")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
body, err := resp.String()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("GET /get → %d (%d bytes)\n", resp.StatusCode, len(body))
|
||||
|
||||
// POST with JSON body.
|
||||
type payload struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
req, err := httpx.NewJSONRequest(ctx, "POST", "/post", payload{
|
||||
Name: "Alice",
|
||||
Email: "alice@example.com",
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
resp, err = client.Do(ctx, req)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer resp.Close()
|
||||
|
||||
fmt.Printf("POST /post → %d\n", resp.StatusCode)
|
||||
}
|
||||
41
examples/form-request/main.go
Normal file
41
examples/form-request/main.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// Demonstrates form-encoded requests for OAuth token endpoints and similar APIs.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"git.codelab.vc/pkg/httpx"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := httpx.New(
|
||||
httpx.WithBaseURL("https://httpbin.org"),
|
||||
)
|
||||
defer client.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Form-encoded POST, common for OAuth token endpoints.
|
||||
req, err := httpx.NewFormRequest(ctx, http.MethodPost, "/post", url.Values{
|
||||
"grant_type": {"client_credentials"},
|
||||
"client_id": {"my-app"},
|
||||
"client_secret": {"secret"},
|
||||
"scope": {"read write"},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
resp, err := client.Do(ctx, req)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer resp.Close()
|
||||
|
||||
body, _ := resp.String()
|
||||
fmt.Printf("Status: %d\nBody: %s\n", resp.StatusCode, body)
|
||||
}
|
||||
54
examples/load-balancing/main.go
Normal file
54
examples/load-balancing/main.go
Normal file
@@ -0,0 +1,54 @@
|
||||
// Demonstrates load balancing across multiple backend endpoints with
|
||||
// circuit breaking and health-checked failover.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"git.codelab.vc/pkg/httpx"
|
||||
"git.codelab.vc/pkg/httpx/balancer"
|
||||
"git.codelab.vc/pkg/httpx/circuitbreaker"
|
||||
"git.codelab.vc/pkg/httpx/retry"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := httpx.New(
|
||||
httpx.WithEndpoints(
|
||||
balancer.Endpoint{URL: "http://localhost:8081", Weight: 3},
|
||||
balancer.Endpoint{URL: "http://localhost:8082", Weight: 1},
|
||||
),
|
||||
httpx.WithBalancer(
|
||||
balancer.WithStrategy(balancer.WeightedRandom()),
|
||||
balancer.WithHealthCheck(
|
||||
balancer.WithHealthInterval(5*time.Second),
|
||||
balancer.WithHealthPath("/healthz"),
|
||||
),
|
||||
),
|
||||
httpx.WithCircuitBreaker(
|
||||
circuitbreaker.WithFailureThreshold(5),
|
||||
circuitbreaker.WithOpenDuration(30*time.Second),
|
||||
),
|
||||
httpx.WithRetry(
|
||||
retry.WithMaxAttempts(3),
|
||||
),
|
||||
httpx.WithLogger(slog.Default()),
|
||||
)
|
||||
defer client.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
for i := range 5 {
|
||||
resp, err := client.Get(ctx, fmt.Sprintf("/api/item/%d", i))
|
||||
if err != nil {
|
||||
log.Printf("request %d failed: %v", i, err)
|
||||
continue
|
||||
}
|
||||
|
||||
body, _ := resp.String()
|
||||
fmt.Printf("request %d → %d: %s\n", i, resp.StatusCode, body)
|
||||
}
|
||||
}
|
||||
54
examples/request-id-propagation/main.go
Normal file
54
examples/request-id-propagation/main.go
Normal file
@@ -0,0 +1,54 @@
|
||||
// Demonstrates request ID propagation between server and client.
|
||||
// The server assigns a request ID to incoming requests, and the client
|
||||
// middleware forwards it to downstream services via X-Request-Id header.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"git.codelab.vc/pkg/httpx"
|
||||
"git.codelab.vc/pkg/httpx/middleware"
|
||||
"git.codelab.vc/pkg/httpx/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
logger := slog.Default()
|
||||
|
||||
// Client that propagates request IDs from context.
|
||||
client := httpx.New(
|
||||
httpx.WithBaseURL("http://localhost:9090"),
|
||||
httpx.WithMiddleware(
|
||||
middleware.RequestID(), // Picks up ID from context, sets X-Request-Id.
|
||||
),
|
||||
)
|
||||
defer client.Close()
|
||||
|
||||
r := server.NewRouter()
|
||||
|
||||
r.HandleFunc("GET /proxy", func(w http.ResponseWriter, r *http.Request) {
|
||||
// The request ID is in r.Context() thanks to server.RequestID().
|
||||
id := server.RequestIDFromContext(r.Context())
|
||||
logger.Info("handling request", "request_id", id)
|
||||
|
||||
// Client automatically forwards the request ID to downstream.
|
||||
resp, err := client.Get(r.Context(), "/downstream")
|
||||
if err != nil {
|
||||
server.WriteError(w, http.StatusBadGateway, fmt.Sprintf("downstream error: %v", err))
|
||||
return
|
||||
}
|
||||
defer resp.Close()
|
||||
|
||||
body, _ := resp.String()
|
||||
server.WriteJSON(w, http.StatusOK, map[string]string{
|
||||
"request_id": id,
|
||||
"downstream_response": body,
|
||||
})
|
||||
})
|
||||
|
||||
// Server with RequestID middleware that assigns IDs to incoming requests.
|
||||
srv := server.New(r, server.Defaults(logger)...)
|
||||
log.Fatal(srv.ListenAndServe())
|
||||
}
|
||||
54
examples/server-basic/main.go
Normal file
54
examples/server-basic/main.go
Normal file
@@ -0,0 +1,54 @@
|
||||
// Basic HTTP server with routing, middleware, health checks, and graceful shutdown.
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"git.codelab.vc/pkg/httpx/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
logger := slog.Default()
|
||||
|
||||
r := server.NewRouter(
|
||||
server.WithNotFoundHandler(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
server.WriteError(w, http.StatusNotFound, "resource not found")
|
||||
})),
|
||||
)
|
||||
|
||||
// Public endpoints.
|
||||
r.HandleFunc("GET /hello", func(w http.ResponseWriter, _ *http.Request) {
|
||||
server.WriteJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Hello, World!",
|
||||
})
|
||||
})
|
||||
|
||||
// API group with shared prefix.
|
||||
api := r.Group("/api/v1")
|
||||
api.HandleFunc("GET /users/{id}", getUser)
|
||||
api.HandleFunc("POST /users", createUser)
|
||||
|
||||
// Health checks.
|
||||
r.Mount("/", server.HealthHandler())
|
||||
|
||||
// Server with production defaults (RequestID → Recovery → Logging).
|
||||
srv := server.New(r, server.Defaults(logger)...)
|
||||
log.Fatal(srv.ListenAndServe())
|
||||
}
|
||||
|
||||
func getUser(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
server.WriteJSON(w, http.StatusOK, map[string]string{
|
||||
"id": id,
|
||||
"name": "Alice",
|
||||
})
|
||||
}
|
||||
|
||||
func createUser(w http.ResponseWriter, r *http.Request) {
|
||||
server.WriteJSON(w, http.StatusCreated, map[string]any{
|
||||
"id": 1,
|
||||
"message": "user created",
|
||||
})
|
||||
}
|
||||
61
examples/server-protected/main.go
Normal file
61
examples/server-protected/main.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// Production server with CORS, rate limiting, body size limits, and timeouts.
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.codelab.vc/pkg/httpx/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
logger := slog.Default()
|
||||
|
||||
r := server.NewRouter(
|
||||
server.WithNotFoundHandler(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
server.WriteError(w, http.StatusNotFound, "not found")
|
||||
})),
|
||||
)
|
||||
|
||||
r.HandleFunc("GET /api/data", func(w http.ResponseWriter, _ *http.Request) {
|
||||
server.WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
})
|
||||
|
||||
r.HandleFunc("POST /api/upload", func(w http.ResponseWriter, r *http.Request) {
|
||||
// Body is already limited by MaxBodySize middleware.
|
||||
server.WriteJSON(w, http.StatusAccepted, map[string]string{"status": "received"})
|
||||
})
|
||||
|
||||
r.Mount("/", server.HealthHandler())
|
||||
|
||||
srv := server.New(r,
|
||||
append(
|
||||
server.Defaults(logger),
|
||||
server.WithMiddleware(
|
||||
// CORS for browser-facing APIs.
|
||||
server.CORS(
|
||||
server.AllowOrigins("https://app.example.com", "https://admin.example.com"),
|
||||
server.AllowMethods("GET", "POST", "PUT", "PATCH", "DELETE"),
|
||||
server.AllowHeaders("Authorization", "Content-Type"),
|
||||
server.ExposeHeaders("X-Request-Id"),
|
||||
server.AllowCredentials(true),
|
||||
server.MaxAge(3600),
|
||||
),
|
||||
// Rate limit: 100 req/s per IP, burst of 200.
|
||||
server.RateLimit(
|
||||
server.WithRate(100),
|
||||
server.WithBurst(200),
|
||||
),
|
||||
// Limit request body to 1 MB.
|
||||
server.MaxBodySize(1<<20),
|
||||
// Per-request timeout of 30 seconds.
|
||||
server.Timeout(30*time.Second),
|
||||
),
|
||||
server.WithAddr(":8080"),
|
||||
)...,
|
||||
)
|
||||
|
||||
log.Fatal(srv.ListenAndServe())
|
||||
}
|
||||
Reference in New Issue
Block a user