Files
httpclient/docs/adr/ADR-003-generic-dojson-helper.md
Rene Nochebuena 6026ab8a5e feat(httpclient): initial stable release v0.9.0
Resilient HTTP client with circuit breaking, exponential-backoff retry, X-Request-ID propagation, and a generic typed JSON helper.

What's included:
- Client interface with Do(req) method; New(logger, cfg) and NewWithDefaults(logger) constructors
- Config struct with env-tag support for timeout, dial timeout, retry, and circuit breaker parameters
- Retry via avast/retry-go/v4 with BackOffDelay; triggers only on network errors and HTTP 5xx
- Circuit breaker via sony/gobreaker wrapping the full retry loop; open circuit → xerrors.ErrUnavailable
- X-Request-ID header propagated automatically from context via logz.GetRequestID on every attempt
- DoJSON[T](ctx, client, req) generic helper for typed JSON request/response with xerrors error mapping
- MapStatusToError(code, msg) exported function mapping HTTP status codes to xerrors types

Tested-via: todo-api POC integration
Reviewed-against: docs/adr/
2026-03-19 13:04:37 +00:00

2.3 KiB

ADR-003: Generic DoJSON[T] Helper for Typed JSON Requests

Status: Accepted Date: 2026-03-18

Context

Calling an HTTP API and decoding the JSON response into a Go struct involves the same boilerplate in every caller: execute the request, check the status code, read the body, unmarshal into the target type. Without a shared helper, this boilerplate is duplicated across services and each copy is a potential source of inconsistent error handling.

Prior to generics (Go 1.18), a typed helper required either an interface{} argument (losing type safety) or code generation.

Decision

A generic top-level function DoJSON[T any] is provided in helpers.go:

func DoJSON[T any](ctx context.Context, client Client, req *http.Request) (*T, error)

It:

  1. Attaches ctx to the request via req.WithContext(ctx).
  2. Calls client.Do(req) — benefits from retry and circuit breaking.
  3. Returns MapStatusToError for any HTTP 4xx or 5xx response.
  4. Reads and unmarshals the body into a T, returning *T on success.
  5. Wraps read and unmarshal failures in xerrors.ErrInternal.

MapStatusToError maps common HTTP status codes to canonical xerrors codes:

  • 404 → ErrNotFound
  • 400 → ErrInvalidInput
  • 401 → ErrUnauthorized
  • 403 → ErrPermissionDenied
  • 409 → ErrAlreadyExists
  • 429 → ErrUnavailable
  • everything else → ErrInternal

DoJSON is a free function, not a method, so it works with any value that satisfies the Client interface (including mocks).

Consequences

Positive:

  • Callers get a fully typed response with one function call and consistent error semantics.
  • The generic type parameter means no type assertions at the call site.
  • MapStatusToError is exported and reusable independently of DoJSON.

Negative:

  • DoJSON always reads the entire body into memory before unmarshalling. Large response bodies should use client.Do directly with streaming JSON decoding.
  • The function returns *T (pointer to decoded value). Callers must nil-check when the response might legitimately be empty (though an empty body would typically produce an unmarshal error).
  • Response headers are not accessible through DoJSON; callers that need headers (e.g. for pagination cursors) must call client.Do directly.