Files
httpclient/CHANGELOG.md
Rene Nochebuena 962b0ccf17 feat(httpclient)!: promote to v1.0.0 — retry-on-429 with Retry-After, DoJSONRequest, bump deps
Extend retry loop to handle HTTP 429 Too Many Requests: when the server includes a
Retry-After header, that duration is used as the retry delay; otherwise falls back to
the configured BackOffDelay. Add DoJSONRequest[Req, Resp] free function that serialises
the request body as JSON, sets Content-Type, and delegates response decoding to DoJSON.
Bump logz and xerrors from v0.9.0 to v1.0.0. API committed as stable.
2026-05-11 19:50:16 -06:00

4.2 KiB

Changelog

All notable changes to this module will be documented in this file.

The format is based on Keep a Changelog, and this module adheres to Semantic Versioning.

1.0.0 — 2026-05-12

Added

  • DoJSONRequest[Req, Resp any](ctx, client, method, rawURL, body) (*Resp, error) — free generic function that serialises body as JSON, constructs the *http.Request with Content-Type: application/json, and delegates response decoding to DoJSON. Complements DoJSON for POST/PUT/PATCH calls; use DoJSON for GET/DELETE where there is no request body.

Changed

  • Retry loop now handles HTTP 429 Too Many Requests: a 429 response triggers a retry (previously only >= 500 did). When the server includes a Retry-After header with an integer seconds value, that duration is used as the retry delay for that attempt; otherwise the configured BackOffDelay applies.
  • logz and xerrors dependencies bumped from v0.9.0 to v1.0.0.

Unchanged

All other API (Client, Config, DefaultConfig, New, NewWithDefaults, DoJSON, MapStatusToError) is API-compatible with v0.9.0.

0.9.0 - 2026-03-18

Added

  • Client interface: Do(req *http.Request) (*http.Response, error).
  • Config struct: fields Name, Timeout (default 30s), DialTimeout (default 5s), MaxRetries (default 3), RetryDelay (default 1s), CBThreshold (default 10), CBTimeout (default 1m); settable via HTTP_* environment variables.
  • DefaultConfig() Config: returns a Config populated with all default values.
  • New(logger logz.Logger, cfg Config) Client: constructs a client with a gobreaker circuit breaker wrapping an avast/retry-go retry loop. The circuit breaker trips after CBThreshold consecutive failures and resets after CBTimeout.
  • NewWithDefaults(logger logz.Logger) Client: convenience constructor; equivalent to New(logger, DefaultConfig()).
  • Retry behaviour: up to MaxRetries attempts with exponential backoff (retry.BackOffDelay). Only network errors and HTTP 5xx responses are retried; 4xx responses are not.
  • Circuit breaker behaviour: the breaker wraps the full retry sequence, so one fully-exhausted retry sequence counts as one failure. State transitions are logged at Warn level with name, from, and to fields.
  • Request ID propagation: logz.GetRequestID(ctx) is called on each attempt; if a request ID is present, it is forwarded as the X-Request-ID request header.
  • Per-request structured logging: successful requests emit an Info log with method, url, status, and latency fields; failed individual attempts emit a Debug log.
  • Error mapping on Do: open circuit → ErrUnavailable; post-retry HTTP error response → MapStatusToError; network/timeout error → ErrInternal.
  • DoJSON[T any](ctx context.Context, client Client, req *http.Request) (*T, error): generic free function that executes a request and decodes the JSON response body into T; returns *T on success and a xerrors-typed error for network failures, HTTP 4xx/5xx responses, unreadable bodies, or JSON decode failures.
  • MapStatusToError(code int, msg string) error (exported): maps HTTP status codes to xerrors codes — 404ErrNotFound, 400ErrInvalidInput, 401ErrUnauthorized, 403ErrPermissionDenied, 409ErrAlreadyExists, 429ErrUnavailable, all others → ErrInternal.
  • Dial timeout applied via a custom net.Dialer on the http.Transport, independent of the per-request Timeout.

Design Notes

  • The circuit breaker wraps the retry loop rather than individual attempts; a full set of exhausted retries registers as a single failure against the breaker threshold, preventing overly aggressive tripping.
  • DoJSON is a free generic function rather than a method so it works with any Client implementation, including mocks, without requiring a concrete type.
  • The module has no lifecycle (no OnInit/OnStart/OnStop) and does not depend on launcher or health; it is a stateless constructor suitable for use at any tier.