# einherjar/httpclient [![version](https://img.shields.io/badge/version-v1.0.0-5C4EE5?style=flat-square)](https://code.nochebuena.dev/einherjar/httpclient) [![license](https://img.shields.io/badge/license-AGPL--3.0-22863A?style=flat-square)](LICENSE) [![go](https://img.shields.io/badge/Go-1.26+-00ADD8?style=flat-square&logo=go&logoColor=white)](https://go.dev) > To cross the realms, one must know the road — and how to wait when the bridge is down. `code.nochebuena.dev/einherjar/httpclient` is the outbound HTTP client component of the Einherjar framework. It composes retry (via `avast/retry-go`) and a circuit breaker (via `sony/gobreaker`) behind a single `Provider` interface with one method: `Do`. Generic helpers `DoJSON` and `DoJSONRequest` reduce boilerplate for JSON APIs without hiding the underlying client. --- ## Usage ### Setup ```go import "code.nochebuena.dev/einherjar/httpclient" // With env-var config client := httpclient.New(logger, httpclient.DefaultConfig()) // Or zero-config with defaults client := httpclient.NewWithDefaults(logger) ``` `httpclient` is not a `lifecycle.Component` — it is stateless and requires no registration with the launcher. ### Sending requests ```go req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.example.com/users", nil) if err != nil { return err } resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() ``` ### JSON GET helper ```go type User struct { ID string `json:"id"` Name string `json:"name"` } req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.example.com/users/123", nil) user, err := httpclient.DoJSON[User](ctx, client, req) // user is *User on success ``` ### JSON POST helper ```go type CreateReq struct { Name string `json:"name"` Email string `json:"email"` } type CreateResp struct { ID string `json:"id"` } resp, err := httpclient.DoJSONRequest[CreateReq, CreateResp]( ctx, client, http.MethodPost, "https://api.example.com/users", CreateReq{Name: "Alice", Email: "alice@example.com"}, ) // resp is *CreateResp on success ``` ### Error mapping ```go // Map HTTP status codes to xerrors for consistent error handling err := httpclient.MapStatusToError(resp.StatusCode, "upstream error") // 400 → ErrInvalidInput // 401 → ErrUnauthorized // 403 → ErrPermissionDenied // 404 → ErrNotFound // 409 → ErrAlreadyExists // 429 → ErrRateLimited // 503 → ErrUnavailable ``` --- ## Environment variables | Variable | Required | Default | Description | |---|---|---|---| | `EINHERJAR_HTTP_CLIENT_NAME` | No | `http` | Circuit breaker name (appears in logs) | | `EINHERJAR_HTTP_TIMEOUT` | No | `30s` | Total request timeout | | `EINHERJAR_HTTP_DIAL_TIMEOUT` | No | `5s` | TCP connection timeout | | `EINHERJAR_HTTP_MAX_RETRIES` | No | `3` | Maximum retry attempts | | `EINHERJAR_HTTP_RETRY_DELAY` | No | `1s` | Delay between retries | | `EINHERJAR_HTTP_CB_THRESHOLD` | No | `10` | Consecutive failures before circuit opens | | `EINHERJAR_HTTP_CB_TIMEOUT` | No | `1m` | Time before circuit attempts half-open | --- ## Dependency graph ``` contracts (zero dependencies) ↑ core ↑ httpclient (contracts, core, retry-go, gobreaker) ↑ your app ``` --- ## Verification ```bash cd httpclient/ go build ./... go vet ./... go test ./... gofmt -l . ``` --- > *A warrior who cannot reach the other realm is useless to the battle.* > *Build the bridge. Make it resilient. Know when to wait.*