• v1.0.0 962b0ccf17

    Rene Nochebuena released this 2026-05-11 19:51:01 -06:00 | 1 commits to main since this release

    v1.0.0

    code.nochebuena.dev/go/httpclient

    Overview

    httpclient v1.0.0 commits the HTTP client API as stable. All v0.9.0 roadmap items are
    resolved. The module wraps net/http with automatic retry, circuit breaking, request-ID
    propagation, and generic typed JSON helpers for both outbound requests and responses.

    What Changed Since v0.9.0

    Retry-on-429 with Retry-After

    The retry loop now handles HTTP 429 Too Many Requests. Previously only network errors and
    >= 500 responses triggered a retry; 429 was returned immediately to the caller.

    When the server responds with 429 and includes a Retry-After header (integer seconds),
    that duration is used as the delay before the next attempt. Without the header, the
    configured BackOffDelay applies as usual.

    // Server responds: 429 Too Many Requests + Retry-After: 5
    // Client waits 5 seconds, then retries automatically.
    

    DoJSONRequest[Req, Resp any]

    New free generic function complementing DoJSON. Use it for POST/PUT/PATCH calls where
    the request also carries a JSON body:

    // Before (manual boilerplate every time):
    data, _ := json.Marshal(myReq)
    req, _ := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(data))
    req.Header.Set("Content-Type", "application/json")
    resp, err := httpclient.DoJSON[MyResponse](ctx, client, req)
    
    // After:
    resp, err := httpclient.DoJSONRequest[MyRequest, MyResponse](ctx, client, "POST", url, myReq)
    

    Use DoJSON for GET/DELETE (no request body); use DoJSONRequest for POST/PUT/PATCH.

    Dependency bumps

    logz and xerrors promoted to v1.0.0.

    Roadmap items resolved

    Item Resolution
    Retry-on-429 with Retry-After Done — respects header when present, falls back to BackOffDelay
    DoJSONRequest[Req, Resp] Done — serialises body, sets Content-Type, delegates to DoJSON
    Circuit breaker state metrics No — exposing gobreaker.State couples the Client interface to a third-party type; state transitions are already logged at Warn level
    DoStream No — streaming is a fundamentally different API contract; use client.Do directly for streaming responses
    Production feedback on defaults Validated as-is — 30s/5s/3 retries/1s delay/10 CB threshold/1m CB timeout confirmed in production

    Full API (stable)

    ClientDo(req *http.Request) (*http.Response, error).

    ConfigName, Timeout, DialTimeout, MaxRetries, RetryDelay, CBThreshold, CBTimeout; env-tag support.

    New(logger logz.Logger, cfg Config) Client — constructor with circuit breaker + retry.

    NewWithDefaults(logger logz.Logger) Client — convenience constructor with production defaults.

    DefaultConfig() Config — returns the default configuration values.

    DoJSON[T any](ctx, client, req) (*T, error) — executes a request and decodes the JSON response into T. Use for GET/DELETE.

    DoJSONRequest[Req, Resp any](ctx, client, method, url, body) (*Resp, error) — serialises body as JSON, constructs the request, and decodes the response. Use for POST/PUT/PATCH.

    MapStatusToError(code int, msg string) error — maps HTTP status codes to xerrors: 404ErrNotFound, 400ErrInvalidInput, 401ErrUnauthorized, 403ErrPermissionDenied, 409ErrAlreadyExists, 429ErrUnavailable, others → ErrInternal.

    Migration from v0.9.0

    No breaking changes. DoJSONRequest is a new addition; existing code using DoJSON is unaffected. The retry-on-429 behavior is a change in Do — callers that previously handled 429 manually after the call should review whether that handling is still needed.

    go get code.nochebuena.dev/go/httpclient@v1.0.0
    
    Downloads
  • v0.9.0 6026ab8a5e

    Rene Nochebuena released this 2026-03-19 07:05:28 -06:00 | 2 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