Add Client with response wrapper, request helpers, and full middleware assembly

Implements the top-level httpx.Client that composes the full chain:
  Logging → User Middlewares → Retry → Circuit Breaker → Balancer → Transport

- Response wrapper with JSON/XML/Bytes decoding and body caching
- NewJSONRequest helper with Content-Type and GetBody support
- Functional options: WithBaseURL, WithTimeout, WithRetry, WithEndpoints, etc.
- Integration tests covering retry, balancing, error mapping, and JSON round-trips
This commit is contained in:
2026-03-20 14:22:22 +03:00
parent a90c4cd7fa
commit f9a05f5c57
8 changed files with 959 additions and 0 deletions

78
request_test.go Normal file
View File

@@ -0,0 +1,78 @@
package httpx_test
import (
"context"
"encoding/json"
"io"
"net/http"
"testing"
"git.codelab.vc/pkg/httpx"
)
func TestNewJSONRequest(t *testing.T) {
t.Run("body is JSON encoded", func(t *testing.T) {
payload := map[string]string{"key": "value"}
req, err := httpx.NewJSONRequest(context.Background(), http.MethodPost, "http://example.com", payload)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
body, err := io.ReadAll(req.Body)
if err != nil {
t.Fatalf("reading body: %v", err)
}
var decoded map[string]string
if err := json.Unmarshal(body, &decoded); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if decoded["key"] != "value" {
t.Errorf("decoded[key] = %q, want %q", decoded["key"], "value")
}
})
t.Run("content type is set", func(t *testing.T) {
req, err := httpx.NewJSONRequest(context.Background(), http.MethodPost, "http://example.com", "test")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ct := req.Header.Get("Content-Type")
if ct != "application/json" {
t.Errorf("Content-Type = %q, want %q", ct, "application/json")
}
})
t.Run("GetBody works", func(t *testing.T) {
payload := map[string]int{"num": 123}
req, err := httpx.NewJSONRequest(context.Background(), http.MethodPost, "http://example.com", payload)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if req.GetBody == nil {
t.Fatal("GetBody is nil")
}
// Read body first time
b1, err := io.ReadAll(req.Body)
if err != nil {
t.Fatalf("reading body: %v", err)
}
// Get a fresh body
body2, err := req.GetBody()
if err != nil {
t.Fatalf("GetBody(): %v", err)
}
b2, err := io.ReadAll(body2)
if err != nil {
t.Fatalf("reading body2: %v", err)
}
if string(b1) != string(b2) {
t.Errorf("GetBody returned different data: %q vs %q", b1, b2)
}
})
}