• v0.9.0 6026ab8a5e

    Rene Nochebuena released this 2026-03-19 07:05:28 -06:00 | 0 commits to main since this release

    v0.9.0

    code.nochebuena.dev/go/httpclient

    Overview

    httpclient is a resilient HTTP client that wraps net/http with automatic retry, circuit
    breaking, request-ID propagation, and a generic typed JSON helper. It is intended for
    application services that make outbound calls to external HTTP APIs and need consistent
    reliability behaviour and error mapping without boilerplate.

    This is the first stable release. The API was designed through multiple architecture reviews
    and validated end-to-end via the todo-api proof-of-concept. It is versioned at v0.9.0 rather
    than v1.0.0 because the library has not yet been exercised in production across all edge cases;
    the pre-1.0 version preserves the option for minor API refinements without a major bump.

    What's Included

    • Client interface with a single Do(req *http.Request) (*http.Response, error) method
    • New(logger, cfg) Client constructor with full Config control
    • NewWithDefaults(logger) Client convenience constructor
    • Config struct with env-tag support (HTTP_CLIENT_NAME, HTTP_TIMEOUT,
      HTTP_DIAL_TIMEOUT, HTTP_MAX_RETRIES, HTTP_RETRY_DELAY, HTTP_CB_THRESHOLD,
      HTTP_CB_TIMEOUT)
    • Retry via github.com/avast/retry-go/v4 with exponential backoff; retries only on
      network errors and HTTP 5xx responses; 4xx responses are not retried
    • Circuit breaker via github.com/sony/gobreaker; trips after CBThreshold consecutive
      failures; open circuit returns xerrors.ErrUnavailable
    • Circuit breaker wraps the entire retry loop: the breaker sees one failure per
      fully-exhausted retry sequence, not per individual attempt
    • X-Request-ID header propagated from context to outbound requests via logz.GetRequestID
    • Duck-typed logz.Logger interface for log injection
    • DoJSON[T](ctx, client, req) (*T, error) generic helper: executes the request, maps
      HTTP 4xx/5xx to xerrors codes, and decodes the JSON body into T
    • MapStatusToError(code int, msg string) error exported function mapping HTTP status codes
      to xerrors types:
      • 404 → ErrNotFound, 400 → ErrInvalidInput, 401 → ErrUnauthorized,
        403 → ErrPermissionDenied, 409 → ErrAlreadyExists, 429 → ErrUnavailable,
        all others → ErrInternal

    Installation

    go get code.nochebuena.dev/go/httpclient@v0.9.0
    

    Requires Go 1.21 or later. Depends on code.nochebuena.dev/go/logz,
    code.nochebuena.dev/go/xerrors, github.com/sony/gobreaker, and
    github.com/avast/retry-go/v4.

    Does not depend on launcher or health — it has no lifecycle and is not a component.

    Design Highlights

    Circuit breaker wraps retry. gobreaker.Execute contains the entire retry.Do loop.
    The breaker counts one failure only when all retries are exhausted, not on each attempt. This
    prevents transient 5xx bursts from immediately tripping the breaker.

    Retry only on 5xx. 4xx responses represent caller errors; retrying them is wasteful and
    can have side effects. Only network errors and HTTP 5xx responses enter the retry loop.

    Request-ID propagation is automatic. logz.GetRequestID(ctx) is called inside the retry
    function on every attempt. No manual header setting is needed in calling code; the header is
    omitted if no ID is present in the context.

    DoJSON[T] is a free function, not a method. It works with any Client implementation,
    including mocks, without requiring a concrete type.

    MapStatusToError is exported and independent. It can be used by any code that already
    has an *http.Response in hand, not just callers that went through DoJSON.

    Known Limitations & Edge Cases

    • No streaming support. DoJSON reads the entire response body into memory.
      client.Do can be used directly for streaming responses, but response body management
      is left to the caller.
    • No multipart or form-encoded body helpers. Only JSON request/response is covered
      by the helpers; other content types require manual construction.
    • Circuit breaker state is per-Client instance. Two Client values targeting the
      same downstream service have independent circuit breakers. To share state across a
      service's request handlers, inject a single Client instance.
    • Retry triggers only on 5xx. Responses in the 4xx range are not retried, even if the
      caller believes the failure is transient (e.g. 429 Too Many Requests with a Retry-After
      header is mapped to ErrUnavailable but not retried automatically).

    v0.9.0 → v1.0.0 Roadmap

    • Evaluate retry-on-429 with Retry-After header respect.
    • Consider exposing circuit breaker state metrics (open/closed/half-open) for observability.
    • Assess whether a streaming variant of DoJSON (e.g. DoStream) is needed.
    • Consider a DoJSONRequest[Req, Resp] helper that also serialises the request body.
    • Gather production feedback on default timeout and retry values before hardening the config.
    Downloads