Guide: HTTP Client¶
This guide walks through the HTTP client example. For the full API reference, see the feature page.
Feature: HTTP Client — typed HTTP calls
examples/adapters-nethttp-client¶
The most comprehensive client demo. Demonstrates both usage patterns in five numbered sections:
- Body — POST /users with a shared contract:
contract.CreateUser.Register(builder)andcontract.CreateUser.ClientHandle()both produce the same typedRouteHandle - 1b. Client-side typed error decode —
CreateUserdeclaresrest.ErrorPattern[EmailConflictError, EmailConflictError](409, ...); callingnethttp.Callwith a duplicate email returns a decodednethttp.ErrorPatternResponseinstead of the untypedUnexpectedStatusError— see "Handling the response" below - Path params — GET /users/{id} with
PathParam.WithCodec(...)codec validated client-side before any HTTP call is sent - Cookies + headers — GET /profile with
CallOptions.CookieParams+CallOptions.HeaderParams; empty or invalid values are rejected pre-flight - Security — GET /data with
CallOptions.CredentialFuncinjectingAuthorization: Bearer <token>; demonstrates all three cases: happy path, no credentials (401), CredentialFunc error (pre-flight abort) - OpenAPI spec — same
rest.Builderused by the server generates the full spec
Observer pattern:
- CountingObserver records calls by HTTP status code (status 0 = pre-flight abort, no request sent)
- RecordValidationError fires per failing field with location = "path", "query", "cookie", "header"
Structured error logging via errors.As + named slog.Logger:
logger := slog.Default().With("transport", "http-client")
var pathErr rest.PathParamError
if errors.As(err, &pathErr) {
logger.Warn("param rejected (no request sent)",
"param", pathErr.Name,
"cause", pathErr.Err,
)
}
→ examples/adapters-nethttp-client
Handling the response: happy path vs error path¶
nethttp.Call (and its convenience wrapper nethttp.CallHandle) always
return exactly (Resp, error) — the "one struct, one call" contract holds
for BOTH directions. There is no partial-success shape to handle: either
you get a fully-decoded, fully-merged Resp, or you get a non-nil error.
Happy path — use the returned value directly¶
user, err := nethttp.Call(ctx, client, baseURL, clientCreate, req, nil, nethttp.CallOptions{})
if err != nil {
// handle the error path — see below
return err
}
// user is fully decoded: body + any response header/cookie merge fields
fmt.Println(user.ID, user.Name)
No status-code check is needed before using the value — any non-2xx
response, decode failure, or pre-flight validation failure is ALWAYS
returned as a non-nil error instead. A nil error guarantees a usable
Resp.
Error path — walk the error chain with errors.As¶
Every failure mode Call can produce is a distinct, errors.As-navigable
typed error. Check them in the order they can occur — pre-flight
(no network call sent) first, then response-side:
_, err := nethttp.Call(ctx, client, baseURL, handle, req, vars, opts)
if err == nil {
return // happy path handled above
}
// Pre-flight: param codec validation failed — no HTTP request was sent.
var pathErr rest.PathParamError
if errors.As(err, &pathErr) {
return fmt.Errorf("invalid %s: %w", pathErr.Name, pathErr.Err)
}
var queryErr rest.QueryParamError
if errors.As(err, &queryErr) { /* ... */ }
var cookieErr rest.CookieParamError
if errors.As(err, &cookieErr) { /* ... */ }
var headerErr rest.HeaderParamError
if errors.As(err, &headerErr) { /* ... */ }
// Pre-flight: request construction/credential failure.
var buildErr nethttp.RequestBuildError
if errors.As(err, &buildErr) { /* malformed base URL, cancelled ctx, ... */ }
// Response-side: the request was sent but failed at the network layer.
var reqErr nethttp.RequestError
if errors.As(err, &reqErr) {
return retry(req) // network/DNS/TLS/timeout — safe to retry
}
// Response-side: a declared rest.ErrorPattern matched and decoded — typed
// business error, decide what to do per Value's concrete type.
var patternResp nethttp.ErrorPatternResponse
if errors.As(err, &patternResp) {
switch v := patternResp.Value.(type) {
case domain.EmailConflictError:
return promptDifferentEmail(v.Email)
default:
return fmt.Errorf("unexpected error payload: %+v", v)
}
}
// Response-side: no ErrorPattern matched (or its body failed to decode) —
// raw status + bytes, the universal fallback.
var statusErr nethttp.UnexpectedStatusError
if errors.As(err, &statusErr) {
return fmt.Errorf("unexpected status %d: %s", statusErr.StatusCode, statusErr.Body)
}
// Response-side: body could not even be read after a successful connection.
var bodyErr nethttp.ResponseBodyError
if errors.As(err, &bodyErr) { /* ... */ }
Rule of thumb for "continuing" after an error:
- Pre-flight param errors (rest.PathParamError/QueryParamError/
CookieParamError/HeaderParamError) mean YOUR request was malformed —
fix the input, never retry as-is.
- nethttp.RequestError is a transport-layer failure (network/DNS/TLS/
timeout) — safe to retry with backoff.
- nethttp.ErrorPatternResponse is a decoded, typed BUSINESS error the
server declared — branch on .Value's concrete type and handle it like
any other domain error (see the "Client-side decode" section in the
REST API feature page).
- nethttp.UnexpectedStatusError is the universal fallback for any
status/body the route didn't declare a typed pattern for — log the raw
status + body, do not assume a specific shape.
Binary requests and responses (PNG, JPEG, PDF…)¶
The client (nethttp.Call) supports binary request bodies and binary response bodies the same way as JSON — register format.Binary on the route handle and the client sets headers and validates automatically.
Sending a binary request body¶
Register format.Binary via WithRequestFormats. The client calls format.Binary.Marshal (validates magic bytes and size), sets Content-Type: image/png, and sends the raw bytes as the request body:
pngCodec := codex.Bytes().
Refine(validate.MaxBytes(5 * 1024 * 1024)).
Refine(validate.PNG)
uploadHandle := uploadRoute.ClientHandle()
uploadHandle.WithRequestFormats(format.Binary(pngCodec).WithContentType("image/png"))
meta, err := nethttp.Call(ctx, client, baseURL, uploadHandle, pngBytes,
map[string]string{"id": imageID},
nethttp.CallOptions{Observer: obs},
)
The Content-Type: image/png header is set automatically from the registered format.
Receiving a binary response body¶
Register format.Binary via WithFormats. The client sets Accept: image/png, reads the raw response body, and calls format.Binary.Unmarshal (validates magic bytes and size before returning):
downloadHandle := downloadRoute.ClientHandle()
downloadHandle.WithFormats(format.Binary(pngCodec).WithContentType("image/png"))
png, err := nethttp.Call(ctx, client, baseURL, downloadHandle, downloadReq,
map[string]string{"id": imageID},
nethttp.CallOptions{Observer: obs},
)
// png is validated (magic bytes + size) — safe to write to disk or display
The Accept: image/png header is set automatically. A server that returns a different Content-Type will cause format.Binary.Unmarshal to fail constraint validation (magic-byte mismatch).
Both directions¶
A route that uploads binary and returns binary registers both:
handle.WithRequestFormats(format.Binary(pngCodec).WithContentType("image/png"))
handle.WithFormats(format.Binary(pngCodec).WithContentType("image/png"))
See examples/png-upload for upload (binary request → JSON response) and download (JSON request → binary response) routes with full codec validation.