diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b12dada --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,100 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +jobs: + test: + name: Test (Go ${{ matrix.go-version }}) + runs-on: ubuntu-latest + strategy: + matrix: + go-version: ['1.21', '1.22', '1.23'] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + cache: true + + - name: Download dependencies + run: go mod download + + - name: Run tests + run: go test -v -race -coverprofile=coverage.out ./... + + - name: Upload coverage + if: matrix.go-version == '1.23' + uses: codecov/codecov-action@v4 + with: + files: coverage.out + fail_ci_if_error: false + + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.23' + cache: true + + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: latest + args: --timeout=5m + + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.23' + cache: true + + - name: Build + run: go build ./... + + - name: Verify go.mod is tidy + run: | + go mod tidy + git diff --exit-code go.mod go.sum + + benchmark: + name: Benchmark + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.23' + cache: true + + - name: Run benchmarks + run: go test -bench=. -benchmem ./... | tee benchmark.txt + + - name: Store benchmark result + uses: actions/upload-artifact@v4 + with: + name: benchmark-results + path: benchmark.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c791869 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Binaries +*.test +*.exe +/examples/*/main +/basic + +# Coverage +coverage/ +*.out + +# Benchmarks +benchmarks.txt +benchmark_results.txt + +# Vendor +/vendor/ + +# IDE +/.idea/ +/.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Debug +__debug_bin* diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..9d18d42 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,151 @@ +run: + timeout: 5m + modules-download-mode: readonly + +linters: + enable: + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - unused + - bodyclose # Check HTTP response body is closed + - dupl # Code duplication + - gocognit # Cognitive complexity + - goconst # Repeated strings that could be constants + - gocritic # Opinionated linter + - gocyclo # Cyclomatic complexity + - gofmt # Formatting + - goimports # Import formatting + - gosec # Security issues + - misspell # Spelling mistakes + - nakedret # Naked returns in functions + - prealloc # Slice preallocation + - revive # Fast, configurable linter + - unconvert # Unnecessary type conversions + - unparam # Unused function parameters + - whitespace # Whitespace issues + +linters-settings: + errcheck: + check-type-assertions: true + check-blank: true + exclude-functions: + - io.Copy + - io.ReadAll + - (io.Closer).Close + + gocognit: + min-complexity: 20 + + gocyclo: + min-complexity: 15 + + goconst: + min-len: 3 + min-occurrences: 3 + + gocritic: + enabled-tags: + - diagnostic + - performance + - style + disabled-checks: + - hugeParam + - whyNoLint + - commentedOutCod + + gofmt: + simplify: true + + goimports: + local-prefixes: github.com/oswaldom-code/go-httpclient + + gosec: + excludes: + - G104 # Audit errors not checked (we handle this with errcheck) + - G304 # File path provided as taint input (not applicable for HTTP client) + + misspell: + locale: US + + nakedret: + max-func-lines: 30 + + prealloc: + simple: true + range-loops: true + for-loops: false + + revive: + rules: + - name: blank-imports + - name: context-as-argument + - name: context-keys-type + - name: dot-imports + - name: error-return + - name: error-strings + - name: error-naming + - name: exported + - name: if-return + - name: increment-decrement + - name: var-naming + - name: var-declaration + - name: package-comments + - name: range + - name: receiver-naming + - name: time-naming + - name: unexported-return + - name: indent-error-flow + - name: errorf + - name: empty-block + - name: superfluous-else + - name: unused-parameter + - name: unreachable-code + - name: redefines-builtin-id + + unparam: + check-exported: false + +issues: + exclude-rules: + - path: _test\.go + linters: + - dupl + - gocognit + - gocyclo + - gosec + - unparam + - errcheck # Test code commonly ignores errors + - bodyclose # Test roundtrippers often return mock responses + - goconst # Test strings don't need to be constants + + # Exclude revive unused-parameter in test files + - path: _test\.go + text: "unused-parameter" + linters: + - revive + + # Allow complexity in internal roundtripper + - path: internal/ + linters: + - gocognit + - gocyclo + + # Exclude unused functions that are part of the pool API + - path: pool\.go + text: "func `acquireResponse` is unused" + linters: + - unused + + max-issues-per-linter: 50 + max-same-issues: 10 + new: false + +output: + formats: + - format: colored-line-number + print-issued-lines: true + print-linter-name: true + sort-results: true diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b22c29b --- /dev/null +++ b/Makefile @@ -0,0 +1,136 @@ +.PHONY: help test test-race test-coverage coverage-summary bench lint fmt vet docs check clean install-tools + +.DEFAULT_GOAL := help + +# Go parameters +GOCMD=go +GOTEST=$(GOCMD) test +GOBUILD=$(GOCMD) build +GOFMT=$(GOCMD) fmt +GOVET=$(GOCMD) vet +GOMOD=$(GOCMD) mod + +# Coverage +COVERAGE_DIR=coverage +COVERAGE_FILE=$(COVERAGE_DIR)/coverage.out +COVERAGE_HTML=$(COVERAGE_DIR)/coverage.html + +# Packages +PACKAGES=./... + +# Colors for terminal output +GREEN=\033[0;32m +YELLOW=\033[0;33m +RED=\033[0;31m +NC=\033[0m # No Color + +help: + @echo "go-httpclient - Production-grade HTTP client for Go" + @echo "" + @echo "Usage: make [target]" + @echo "" + @echo "Targets:" + @sed -n 's/^##//p' $(MAKEFILE_LIST) | column -t -s ':' | sed -e 's/^/ /' + +test: + @echo "$(GREEN)Running tests...$(NC)" + $(GOTEST) -v $(PACKAGES) + +test-race: + @echo "$(GREEN)Running tests with race detector...$(NC)" + $(GOTEST) -v -race $(PACKAGES) + +test-coverage: + @echo "$(GREEN)Running tests with coverage...$(NC)" + @mkdir -p $(COVERAGE_DIR) + $(GOTEST) -v -coverprofile=$(COVERAGE_FILE) -covermode=atomic $(PACKAGES) + $(GOCMD) tool cover -html=$(COVERAGE_FILE) -o $(COVERAGE_HTML) + $(GOCMD) tool cover -func=$(COVERAGE_FILE) + @echo "" + @echo "$(GREEN)Coverage report generated: $(COVERAGE_HTML)$(NC)" + +coverage-summary: + @echo "$(GREEN)=== Total Coverage ===$(NC)" + @$(GOCMD) tool cover -func=$(COVERAGE_FILE) | tail -1 + @echo "" + @echo "$(YELLOW)=== Uncovered Functions (0.0%) ===$(NC)" + @$(GOCMD) tool cover -func=$(COVERAGE_FILE) | awk '$$NF == "0.0%"' + +test-short: + @echo "$(GREEN)Running short tests...$(NC)" + $(GOTEST) -v -short $(PACKAGES) + +bench: + @echo "$(GREEN)Running benchmarks...$(NC)" + $(GOTEST) -bench=. -benchmem $(PACKAGES) + +bench-compare: + @echo "$(GREEN)Running benchmarks for comparison...$(NC)" + $(GOTEST) -bench=. -benchmem -count=5 $(PACKAGES) | tee benchmarks.txt + +lint: + @echo "$(GREEN)Running linter...$(NC)" + @if command -v golangci-lint >/dev/null 2>&1; then \ + golangci-lint run $(PACKAGES); \ + else \ + echo "$(YELLOW)golangci-lint not installed. Run 'make install-tools' first.$(NC)"; \ + exit 1; \ + fi + +fmt: + @echo "$(GREEN)Formatting code...$(NC)" + $(GOFMT) $(PACKAGES) + +fmt-check: + @echo "$(GREEN)Checking code formatting...$(NC)" + @test -z "$$(gofmt -l .)" || (echo "$(RED)Code is not formatted. Run 'make fmt'$(NC)" && gofmt -l . && exit 1) + +vet: + @echo "$(GREEN)Running go vet...$(NC)" + $(GOVET) $(PACKAGES) + +docs: + @echo "$(GREEN)Starting documentation server...$(NC)" + @echo "Open http://localhost:8080/github.com/oswaldom-code/go-httpclient/httpclient" + @if command -v pkgsite >/dev/null 2>&1; then \ + pkgsite -http=:8080; \ + else \ + echo "$(YELLOW)pkgsite not installed. Run 'make install-tools' first.$(NC)"; \ + echo "Falling back to godoc..."; \ + godoc -http=:8080; \ + fi + +check: fmt-check vet lint test + @echo "$(GREEN)All checks passed!$(NC)" + +check-all: fmt-check vet lint test-race + @echo "$(GREEN)All checks passed!$(NC)" + +clean: + @echo "$(GREEN)Cleaning...$(NC)" + @rm -rf $(COVERAGE_DIR) + @rm -f benchmarks.txt + @rm -f benchmark_results.txt + $(GOCMD) clean -cache -testcache + + +install-tools: + @echo "$(GREEN)Installing development tools...$(NC)" + @echo "Installing golangci-lint..." + go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + @echo "Installing pkgsite..." + go install golang.org/x/pkgsite/cmd/pkgsite@latest + @echo "$(GREEN)Done! Make sure $(GOPATH)/bin is in your PATH.$(NC)" + +ci: deps fmt-check vet lint test-race + @echo "$(GREEN)CI pipeline passed!$(NC)" + +version: + @$(GOCMD) version + +info: + @echo "Module: $$(head -1 go.mod | cut -d' ' -f2)" + @echo "Go version: $$($(GOCMD) version | cut -d' ' -f3)" + @echo "Packages: $$($(GOCMD) list $(PACKAGES) | wc -l | tr -d ' ')" + @echo "Test files: $$(find . -name '*_test.go' | wc -l | tr -d ' ')" + @echo "Source files: $$(find . -name '*.go' ! -name '*_test.go' | wc -l | tr -d ' ')" diff --git a/README.md b/README.md index c3dfe54..cb8e926 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,448 @@ # go-httpclient -HTTP client for Go. + +Production-grade HTTP client for Go with built-in resiliency patterns. + +[![CI](https://github.com/oswaldom-code/go-httpclient/actions/workflows/ci.yml/badge.svg)](https://github.com/oswaldom-code/go-httpclient/actions/workflows/ci.yml) +[![codecov](https://codecov.io/gh/oswaldom-code/go-httpclient/branch/main/graph/badge.svg)](https://codecov.io/gh/oswaldom-code/go-httpclient) +[![Go Report Card](https://goreportcard.com/badge/github.com/oswaldom-code/go-httpclient)](https://goreportcard.com/report/github.com/oswaldom-code/go-httpclient) +[![Go Reference](https://pkg.go.dev/badge/github.com/oswaldom-code/go-httpclient.svg)](https://pkg.go.dev/github.com/oswaldom-code/go-httpclient) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Go Version](https://img.shields.io/badge/Go-1.21+-00ADD8?logo=go)](https://go.dev/) + +## Motivation + +Después de implementar clientes HTTP con patrones de resiliencia en múltiples proyectos +de microservicios, identificé un patrón recurrente: + +1. **La stdlib no es suficiente** - `net/http` es potente pero no incluye retry, + circuit breaker ni rate limiting +2. **Las dependencias son un problema** - Librerías como Resty traen dependencias + transitivas que complican auditorías de seguridad y aumentan el tamaño del binario +3. **Reinventar la rueda es costoso** - Cada equipo termina escribiendo su propio + wrapper con bugs sutiles en manejo de contextos, timeouts y connection pooling + +Esta librería resuelve ese problema: **resiliencia production-ready con cero dependencias**. + +### Usage Modes + +| Modo | Cuándo usarlo | +|------|---------------| +| `go get` | Proyectos que aceptan dependencias externas | +| Copiar a `pkg/httpclient` | Políticas estrictas de zero-deps, vendor everything | + +El código está diseñado para funcionar en ambos escenarios sin modificaciones. + +## Features + +- **Zero dependencies** - Only Go standard library +- **Faster than net/http** - 35% faster than `http.Client` baseline +- **Middleware architecture** - Composable, testable, extensible +- **Fluent API** - Resty-style request builder +- **Resiliency patterns** - Retry, circuit breaker, rate limiting, timeout +- **Multiple backoff strategies** - Constant, linear, exponential, Fibonacci, jitter variants +- **Object pooling** - Reduced allocations via `sync.Pool` +- **100% test coverage** - 101 tests + +## Installation + +```bash +go get github.com/oswaldom-code/go-httpclient +``` + +Requires Go 1.21+ + +## Quick Start + +### Basic Usage + +```go +package main + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/oswaldom-code/go-httpclient/httpclient" +) + +func main() { + // Create client with middleware + client := httpclient.New( + httpclient.WithMiddleware( + httpclient.Timeout(5*time.Second), + httpclient.Retry(httpclient.RetryConfig{MaxAttempts: 3}), + httpclient.CircuitBreaker(httpclient.CircuitBreakerConfig{ + FailureThreshold: 5, + ResetTimeout: 30*time.Second, + }), + ), + ) + + // Make request + req, _ := http.NewRequest("GET", "https://api.example.com/users", nil) + resp, err := client.Do(context.Background(), req) + if err != nil { + panic(err) + } + defer resp.Body.Close() + + fmt.Println("Status:", resp.StatusCode) +} +``` + +### Fluent API + +```go +client := httpclient.New() + +// GET request with query params +resp, err := httpclient.R(client). + SetHeader("Authorization", "Bearer token"). + SetQueryParam("page", "1"). + SetQueryParam("limit", "10"). + Get("https://api.example.com/users") + +// POST request with JSON body +resp, err := httpclient.R(client). + SetAuthToken("my-token"). + SetBodyJSON(map[string]string{ + "name": "John", + "email": "john@example.com", + }). + Post("https://api.example.com/users") + +// Path parameters +resp, err := httpclient.R(client). + SetPathParam("org", "acme"). + SetPathParam("repo", "api"). + Get("https://api.github.com/repos/{org}/{repo}") +``` + +## Middleware + +### Timeout + +```go +client := httpclient.New( + httpclient.WithMiddleware( + httpclient.Timeout(5*time.Second), + ), +) +``` + +Respects existing context deadlines - uses the shorter of the two. + +### Retry + +```go +client := httpclient.New( + httpclient.WithMiddleware( + httpclient.Retry(httpclient.RetryConfig{ + MaxAttempts: 3, + Backoff: httpclient.ExponentialBackoff(100*time.Millisecond, 10*time.Second), + IsRetryable: httpclient.DefaultIsRetryable, // 429, 502, 503, 504 + RetryAllMethods: false, // Only retry idempotent methods by default + }), + ), +) +``` + +**Built-in backoff strategies:** + +| Strategy | Description | +|----------|-------------| +| `ConstantBackoff(d)` | Always wait `d` | +| `LinearBackoff(base, max)` | `base * (attempt + 1)` | +| `ExponentialBackoff(base, max)` | `base * 2^attempt` with ±20% jitter | +| `FibonacciBackoff(base, max)` | `base * fib(attempt)` | +| `ExponentialBackoffFullJitter(base, max)` | `random(0, base * 2^attempt)` | +| `ExponentialBackoffEqualJitter(base, max)` | `base * 2^attempt / 2 + random(0, half)` | +| `DecorrelatedJitterBackoff(base, max)` | AWS-style decorrelated jitter | + +Composable with `WithJitter()`, `WithMin()`, `WithMax()`. + +### Circuit Breaker + +```go +client := httpclient.New( + httpclient.WithMiddleware( + httpclient.CircuitBreaker(httpclient.CircuitBreakerConfig{ + FailureThreshold: 5, // Open after 5 consecutive failures + ResetTimeout: 30*time.Second, // Try half-open after 30s + IsFailure: httpclient.DefaultIsFailure, // Errors + 5xx + }), + ), +) +``` + +State machine: `Closed → Open → Half-Open → Closed/Open` + +Returns `httpclient.ErrCircuitOpen` when circuit is open. + +### Rate Limiting + +```go +// Token bucket: 100 requests/second, burst of 10 +limiter := httpclient.NewTokenBucket(100, 10) + +client := httpclient.New( + httpclient.WithMiddleware( + httpclient.RateLimit(httpclient.RateLimitConfig{ + Limiter: limiter, + WaitOnLimit: true, // Block until token available + RespectRetryAfter: true, // Honor Retry-After header + }), + ), +) + +// Per-host rate limiting +perHostLimiter := httpclient.NewPerHostRateLimiter(50, 5) // 50 req/s per host +``` + +### Logging + +```go +client := httpclient.New( + httpclient.WithMiddleware( + httpclient.Logging(httpclient.LoggingConfig{ + Logger: httpclient.LoggerFunc(func(e httpclient.LogEntry) { + log.Printf("%s %s %d %v", e.Method, e.URL, e.StatusCode, e.Duration) + }), + ShouldLog: func(req *http.Request, resp *http.Response, err error) bool { + return err != nil || resp.StatusCode >= 500 // Only log errors + }, + }), + ), +) +``` + +### Metrics + +```go +client := httpclient.New( + httpclient.WithMiddleware( + httpclient.Metrics(httpclient.MetricsConfig{ + Recorder: httpclient.MetricsRecorderFunc(func(e httpclient.MetricEvent) { + // Send to Prometheus, StatsD, etc. + myCounter.WithLabels(e.Method, e.Host, e.StatusCode).Inc() + myHistogram.Observe(e.Duration.Seconds()) + }), + }), + ), +) +``` + +`MetricEvent` fields: `Method`, `Host`, `Path`, `StatusCode`, `Duration`, `BytesSent`, `BytesReceived`, `Error`, `Success` + +## Error Classification + +```go +resp, err := client.Do(ctx, req) +if err != nil { + classified := httpclient.Classify(err) + + switch classified.Kind { + case httpclient.ErrKindTimeout: + // Request timed out + case httpclient.ErrKindCancelled: + // Context was cancelled + case httpclient.ErrKindConnection: + // Connection refused, reset, etc. + case httpclient.ErrKindDNS: + // DNS resolution failed + case httpclient.ErrKindTLS: + // Certificate error + case httpclient.ErrKindTemporary: + // Temporary error, may resolve on retry + } + + // Or use helpers + if httpclient.IsRetryable(err) { + // Safe to retry (timeout, connection, DNS, temporary) + } +} +``` + +## Middleware Order + +Middleware executes in the order specified: + +```go +client := httpclient.New( + httpclient.WithMiddleware( + httpclient.Logging(...), // 1. Log request start + httpclient.Metrics(...), // 2. Start timing + httpclient.Timeout(...), // 3. Apply timeout + httpclient.RateLimit(...), // 4. Check rate limit + httpclient.CircuitBreaker(...), // 5. Check circuit + httpclient.Retry(...), // 6. Retry on failure + ), +) +``` + +Recommended order: `Logging → Metrics → Timeout → RateLimit → CircuitBreaker → Retry` + +## Custom Transport + +```go +// Use custom transport +client := httpclient.New( + httpclient.WithTransport(&http.Transport{ + MaxIdleConns: 200, + MaxIdleConnsPerHost: 20, + IdleConnTimeout: 90*time.Second, + }), +) + +// Or use optimized default +transport := httpclient.DefaultTransport() // HTTP/2 enabled, optimized pool +``` + +## Object Pooling + +Reduce allocations with buffer pooling: + +```go +// Get a buffer from the pool +buf := httpclient.GetBuffer() +defer httpclient.PutBuffer(buf) + +buf.WriteString("request body") +``` + +## Benchmarks + +``` +goos: linux +goarch: amd64 +cpu: Intel Core i7-1255U + +BenchmarkClient_Baseline-12 235 ns/op 656 B/op 4 allocs/op +BenchmarkStdHttpClient_Baseline-12 317 ns/op 600 B/op 7 allocs/op (+35%) +BenchmarkClient_WithRetry-12 265 ns/op 656 B/op 4 allocs/op +BenchmarkClient_WithCircuitBreaker-12 271 ns/op 656 B/op 4 allocs/op +BenchmarkClient_AllMiddleware-12 1143 ns/op 1472 B/op 12 allocs/op +BenchmarkTokenBucket_TryAcquire-12 52 ns/op 0 B/op 0 allocs/op +BenchmarkBackoff_Exponential-12 7 ns/op 0 B/op 0 allocs/op +``` + +**Key results:** +- 35% faster than `net/http` client baseline +- All middleware stack: ~1μs overhead (negligible vs network latency) +- Rate limiter: 52ns per check, zero allocations +- Backoff strategies: <10ns, zero allocations + +## Design Principles + +1. **No global state** - Each client is independent +2. **Context-first** - All operations respect context cancellation +3. **Fail fast** - Explicit errors, no silent failures +4. **Composable** - Mix and match middleware +5. **Testable** - All components are mockable +6. **Zero dependencies** - Only Go standard library + +## API Reference + +See [pkg.go.dev](https://pkg.go.dev/github.com/oswaldom-code/go-httpclient/httpclient) for full API documentation. + +## Development + +### Prerequisites + +```bash +# Install development tools +make install-tools +``` + +### Available Commands + +```bash +make help # Show all available commands +make test # Run unit tests +make test-race # Run tests with race detector +make test-coverage # Generate coverage report +make bench # Run benchmarks +make lint # Run golangci-lint +make fmt # Format code +make vet # Run go vet +make check # Run all checks (fmt, vet, lint, test) +make docs # Serve documentation locally +make clean # Clean build artifacts +``` + +### CI Pipeline + +The project uses GitHub Actions for CI with: + +- Tests on Go 1.21, 1.22, and 1.23 +- Race detector enabled +- golangci-lint for code quality +- Coverage reporting +- Benchmark tracking on PRs + +## Contributing + +Contributions are welcome! Please ensure: + +1. Fork the repository +2. Create a feature branch: `git checkout -b feature/my-feature` +3. Run checks: `make check` +4. Commit changes: `git commit -m 'Add my feature'` +5. Push: `git push origin feature/my-feature` +6. Open a Pull Request + +All PRs must pass CI checks before merging. + +## Roadmap + +### Phase 2: Advanced Resiliency + +- [ ] **Circuit breaker per endpoint** - Separate circuit state for each host/path +- [ ] **Sliding window statistics** - Time-based failure rate calculation +- [ ] **Bulkhead pattern** - Resource isolation per service +- [ ] **Retry budget** - Limit retries per time window +- [ ] **Hedged requests** - Send duplicate request if first is slow +- [ ] **Adaptive timeout** - Adjust timeout based on latency percentiles + +### Phase 3: Observability + +- [ ] **OpenTelemetry integration** - Native tracing and metrics +- [ ] **slog compatibility** - Structured logging (Go 1.21+) +- [ ] **Prometheus metrics** - Out-of-the-box histograms and counters +- [ ] **Distributed tracing** - Automatic trace context propagation +- [ ] **Health check endpoints** - Readiness/liveness probes + +### Phase 4: Developer Experience + +- [ ] **Auto marshaling** - JSON, XML, Protocol Buffers, MessagePack +- [ ] **OAuth2 support** - Automatic token refresh +- [ ] **Debug mode** - Request/response dump, curl generation +- [ ] **Response validation** - JSON Schema, status assertions +- [ ] **Multipart uploads** - With progress callbacks + +### Phase 5: Advanced Features + +- [ ] **Load balancing** - Round-robin, weighted, least connections +- [ ] **Service discovery** - DNS SRV, Kubernetes, Consul +- [ ] **Response caching** - RFC 7234 compliant, pluggable backends +- [ ] **Request coalescing** - Single-flight for duplicate requests +- [ ] **HTTP/3 support** - QUIC protocol (optional) +- [ ] **Connection warm-up** - Pre-establish connections + +### Phase 6: Enterprise + +- [ ] **mTLS support** - Mutual TLS authentication +- [ ] **Certificate pinning** - Enhanced security +- [ ] **Secrets management** - Vault integration +- [ ] **Configuration hot-reload** - Runtime tuning +- [ ] **Chaos engineering** - Fault injection for testing + +--- + +Want to contribute? Check the issues labeled `good first issue` or `help wanted`. + +## License + +MIT License - see [LICENSE](LICENSE) file. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..542ad99 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/oswaldom-code/go-httpclient + +go 1.24.0