Rene Nochebuena caa397591e feat(httpclient): initial implementation — HTTP client with retry and circuit breaker (v1.0.0)
Introduces code.nochebuena.dev/einherjar/httpclient — the outbound HTTP client
starter for the Einherjar framework. Absorbs the httpclient package from micro-lib,
replacing fmt.Errorf wrapping with core/xerrors and adding generic JSON helpers.

Interfaces:
- Provider — Do(req *http.Request) (*http.Response, error)

Implementation:
- New(logger, cfg) Provider — configures net.Dialer + retry + circuit breaker
- NewWithDefaults(logger) Provider — convenience constructor with default config
- Retry: avast/retry-go; configurable MaxRetries and RetryDelay; retries on
  network errors and 5xx responses; logs each retry attempt at Warn level
- Circuit breaker: sony/gobreaker; opens after CBThreshold consecutive failures
  within CBTimeout window; returns ErrUnavailable when open
- DoJSON[T](ctx, client, req) (*T, error) — executes request, decodes JSON body
- DoJSONRequest[Req, Resp](ctx, client, method, rawURL, body) (*Resp, error) —
  marshals body, builds request, executes, decodes response
- MapStatusToError(code, msg) error — maps HTTP status codes to xerrors values

Config (EINHERJAR_HTTP_* env vars):
  Name(http), Timeout(30s), DialTimeout(5s),
  MaxRetries(3), RetryDelay(1s), CBThreshold(10), CBTimeout(1m)

- identifiable.go: package-level Module variable (observability.Identifiable) for version
  identification — httpclient is a stateless provider; not registered with the launcher
2026-05-29 16:06:47 +00:00

einherjar/httpclient

version license go

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

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

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

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

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

// 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

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.

Description
HTTP client with retry, circuit breaker, and generic JSON helpers
Readme 59 KiB
2026-05-29 10:07:06 -06:00
Languages
Go 100%