Documentation
¶
Index ¶
- Constants
- Variables
- func EnsureTraceID(ctx context.Context) string
- func GenerateTraceParent() string
- func IsErrorType(err error, errorType ErrorType) bool
- func IsHTTPStatusError(err error, statusCode int) bool
- func IsJOSEError(err error) bool
- func IsSuccessStatus(statusCode int) bool
- func NewClientTLSConfig(cfg *ClientTLSConfig) (*tls.Config, error)
- func TraceIDFromContext(ctx context.Context) (string, bool)
- func TraceParentFromContext(ctx context.Context) (string, bool)
- func TraceStateFromContext(ctx context.Context) (string, bool)
- func WithTraceID(ctx context.Context, traceID string) context.Context
- func WithTraceParent(ctx context.Context, traceParent string) context.Context
- func WithTraceState(ctx context.Context, traceState string) context.Context
- type BasicAuth
- type BodyEnvelope
- type Builder
- func (b *Builder) Build() (Client, error)
- func (b *Builder) WithBasicAuth(username, password string) *Builder
- func (b *Builder) WithDefaultHeader(key, value string) *Builder
- func (b *Builder) WithHTTPClient(client *nethttp.Client) *Builder
- func (b *Builder) WithJOSE(cfg JOSEConfig) *Builder
- func (b *Builder) WithLogPayloads(enabled bool) *Builder
- func (b *Builder) WithMaxPayloadLogBytes(n int) *Builder
- func (b *Builder) WithPeerName(name string) *Builder
- func (b *Builder) WithRequestInterceptor(interceptor RequestInterceptor) *Builder
- func (b *Builder) WithResponseInterceptor(interceptor ResponseInterceptor) *Builder
- func (b *Builder) WithRetries(maxRetries int, retryDelay time.Duration) *Builder
- func (b *Builder) WithTLSConfig(tlsCfg *tls.Config) *Builder
- func (b *Builder) WithTimeout(timeout time.Duration) *Builder
- func (b *Builder) WithTraceIDExtractor(extractor func(_ context.Context) (string, bool)) *Builder
- func (b *Builder) WithTraceIDGenerator(gen func() string) *Builder
- func (b *Builder) WithTraceIDHeader(name string) *Builder
- func (b *Builder) WithTransport(transport nethttp.RoundTripper) *Builder
- func (b *Builder) WithW3CTrace(enabled bool) *Builder
- type Client
- type ClientError
- func NewHTTPError(message string, statusCode int, body []byte) ClientError
- func NewInterceptorError(message, stage string, wrapped error) ClientError
- func NewNetworkError(message string, wrapped error) ClientError
- func NewTimeoutError(message string, timeout time.Duration) ClientError
- func NewValidationError(message, field string) ClientError
- type ClientTLSConfig
- type Config
- type ErrorType
- type JOSEConfig
- type JOSETransport
- type Request
- type RequestInterceptor
- type Response
- type ResponseInterceptor
- type Stats
Constants ¶
const ( // DefaultTimeout is the default request timeout duration DefaultTimeout = 30 * time.Second // DefaultMaxRetries is the default maximum number of retries for failed requests DefaultMaxRetries = 0 // DefaultRetryDelay is the default delay between retries DefaultRetryDelay = 1 * time.Second )
const ( // HeaderXRequestID is the standard header name for request tracing HeaderXRequestID = gobrickstrace.HeaderXRequestID // HeaderTraceParent is the W3C trace context header name HeaderTraceParent = gobrickstrace.HeaderTraceParent // HeaderTraceState is the W3C trace context "tracestate" header name HeaderTraceState = gobrickstrace.HeaderTraceState )
const DefaultMaxJOSEBodyBytes int64 = 10 << 20 // 10 MiB
DefaultMaxJOSEBodyBytes caps the size of an inbound JOSE response body when no explicit MaxResponseBytes is set on JOSETransport. 10 MiB is comfortably larger than any expected VTS-style payload (token responses are typically <2 KiB) but small enough to bound peak memory if a counterparty (or attacker) sends a malicious response. Defense-in-depth against memory exhaustion.
Variables ¶
var ErrUnsafeTransportComposition = errors.New("unsafe transport composition")
ErrUnsafeTransportComposition is returned by Build when a builder composition would silently discard TLS material or a caller-supplied transport. Match it with errors.Is, not a message-text check — see ADR-044.
Functions ¶
func EnsureTraceID ¶
EnsureTraceID returns an existing trace ID from context or generates a new one
func GenerateTraceParent ¶
func GenerateTraceParent() string
GenerateTraceParent creates a minimal W3C traceparent header value. Format: version(2)-trace-id(32)-span-id(16)-flags(2), e.g., "00-<32>-<16>-01"
func IsErrorType ¶
IsErrorType checks if an error is of a specific type
func IsHTTPStatusError ¶
IsHTTPStatusError checks if an error is an HTTP error with a specific status code
func IsJOSEError ¶ added in v0.30.0
IsJOSEError reports whether err is a JOSE crypto failure — kept as a thin re-export of jose.IsError for discoverability from the httpclient package, since transport callers typically already import httpclient and may not realize the canonical helper lives in jose.
func IsSuccessStatus ¶
IsSuccessStatus checks if a status code represents success (2xx). Uses stdlib constants for self-documenting boundaries.
func NewClientTLSConfig ¶ added in v0.55.0
func NewClientTLSConfig(cfg *ClientTLSConfig) (*tls.Config, error)
NewClientTLSConfig loads the declared material into a *tls.Config with a TLS 1.2 floor. It never disables certificate verification; the explicit escape hatch for local testing is passing a hand-built *tls.Config to WithTLSConfig.
func TraceIDFromContext ¶
TraceIDFromContext returns a trace ID from context if present
func TraceParentFromContext ¶
TraceParentFromContext returns a traceparent from context if present
func TraceStateFromContext ¶
TraceStateFromContext returns a tracestate from context if present
func WithTraceID ¶
WithTraceID adds a trace ID to the context for HTTP client propagation
func WithTraceParent ¶
WithTraceParent adds a W3C traceparent value to the context
Types ¶
type BodyEnvelope ¶ added in v0.64.0
type BodyEnvelope interface {
// Wrap builds the outbound request body from the compact jose.Seal produced and names
// the Content-Type to advertise; an empty content type sends no Content-Type header at
// all. An error aborts the round trip: no request is sent. Consulted only when Outbound
// is set.
Wrap(compact string) (body []byte, contentType string, err error)
// Unwrap recognizes and extracts a compact from a buffered response body, given the
// response Content-Type. Returning ok=false passes the body through untouched.
// Consulted only when Inbound is set, and it replaces the application/jose
// Content-Type rule, so EVERY eligible response body is buffered before it runs.
Unwrap(contentType string, body []byte) (compact string, ok bool)
}
BodyEnvelope shapes the sealed body on the wire for counterparties that do not carry the compact JOSE serialization on its own -- Visa Message Level Encryption's {"encData":"<compact>"} JSON object, for example.
A nil BodyEnvelope is the identity: the compact JWE is the request body with a Content-Type of application/jose, and only application/jose responses are unwrapped.
One interface rather than two function fields, so JOSETransport and JOSEConfig stay comparable values -- a func field is not comparable, and both structs are part of the package's exported surface.
func VisaMLEEnvelope ¶ added in v0.64.0
func VisaMLEEnvelope() BodyEnvelope
VisaMLEEnvelope returns the BodyEnvelope implementing Visa Message Level Encryption's JSON envelope: outbound bodies are {"encData":"<compact JWE>"} sent as application/json, and inbound bodies are recognized by shape rather than Content-Type — any JSON object carrying a non-empty string encData member is unwrapped, and everything else passes through untouched. Unknown sibling members are ignored.
type Builder ¶
type Builder struct {
// contains filtered or unexported fields
}
Builder provides a fluent interface for configuring the REST client
func NewBuilder ¶
NewBuilder creates a new client builder.
Panics if log is nil, or is a non-nil interface holding a nil pointer. Every built client's request/response logging path dereferences the logger unguarded, so a nil logger is a wiring error that would otherwise surface as a panic on the first request — in production, and on the AMQP consumer path as a nack-without-requeue — dead-lettered when the queue was declared with a DLQ, dropped otherwise. The remaining boundary: a non-nil logger that panics internally (e.g. a partially implemented double) is still the caller's contract to keep.
func (*Builder) Build ¶
Build creates the REST client. It returns an error wrapping ErrUnsafeTransportComposition on a base-transport-slot displacement — see ADR-044 for scope (what counts, what doesn't). A WithJOSE policy that fails jose.Policy.Validate is reported on a separate error path that carries the underlying *jose.Error (match it with errors.As), not that sentinel.
func (*Builder) WithBasicAuth ¶
WithBasicAuth sets basic authentication credentials
func (*Builder) WithDefaultHeader ¶
WithDefaultHeader adds a default header that will be sent with all requests
func (*Builder) WithHTTPClient ¶
WithHTTPClient allows providing a custom *http.Client instance. Build shallow-copies the provided client and never mutates it: the caller's client keeps its own Transport and Timeout. If the provided client's Timeout is zero, the copy gets the builder's configured Timeout. The copy's Transport is preserved unless explicitly overridden via WithTransport or WithTLSConfig, both of which fill the base-transport slot directly. A WithJOSE wrapper registered with no such base, against a client that already carries its own Transport, does not silently override it either — Build fails with an error wrapping ErrUnsafeTransportComposition instead (see discardsClientTransport). The copy is shallow: reference fields such as the cookie Jar remain shared with the caller's client.
func (*Builder) WithJOSE ¶ added in v0.30.0
func (b *Builder) WithJOSE(cfg JOSEConfig) *Builder
WithJOSE configures a JOSETransport that seals outbound request bodies and opens application/jose response bodies — sign+encrypt and decrypt+verify under the nested JWE-of-JWS default, encrypt-only and decrypt-only under a SealModeBareJWE policy. Both directions are optional and one-directional protection is a supported shape: pass cfg.Inbound = nil when the counterparty does not return JOSE-wrapped responses, and cfg.Outbound = nil to leave request bodies untouched. An Envelope needs at least one of the two, or Build fails with JOSE_POLICY_ENVELOPE_UNPAIRED.
Composition: transport layers are applied at Build time in a fixed order — the base transport from WithTransport or WithTLSConfig is innermost, request signers next, and body transforms such as JOSE outermost — so the result does not depend on the order these options were called. A Transport configured directly on the *http.Client passed to WithHTTPClient is REPLACED, not wrapped: the chain then has no base and dials via nethttp.DefaultTransport, losing client certificates, pinned roots and proxy settings (Build returns an error). Always fill the base slot — with WithTransport, or with WithTLSConfig when the base is a TLS config.
Body envelopes: cfg.Envelope moves the compact into and out of a counterparty's own body format — httpclient.VisaMLEEnvelope() returns the one for Visa Message Level Encryption. A hook without the policy it serves fails Build.
Per-attempt freshness: because httpclient retries by re-running the request build loop, each retry produces a freshly-sealed payload — useful for protocols that require unique iat/jti claims per attempt.
Validation: Build fills each non-nil policy's unset algorithms from the jose package defaults and runs Policy.Validate, so a disallowed algorithm, a kid missing for the policy's direction, or a nil Resolver fails construction rather than every request. Kids are NOT resolved at Build time: a resolver may be backed by lazily-loaded key material, so an unknown kid still surfaces per request.
Calling this twice does not stack two JOSE layers: the last call wins outright, as it does for the base-transport slot. The body is sealed exactly once, with the config from the final call — including its nil fields.
func (*Builder) WithLogPayloads ¶ added in v0.37.0
WithLogPayloads enables debug-level logging of request and response bodies. WARNING: enable only in development/staging — even with SensitiveDataFilter active, JSON bodies are parsed and field-masked but the full body surface is widened significantly. Non-JSON bodies (form-urlencoded, binary, etc.) are never emitted; only the content-type and byte count are logged to prevent credential pairs from reaching the log stream.
func (*Builder) WithMaxPayloadLogBytes ¶ added in v0.37.0
WithMaxPayloadLogBytes caps the number of body bytes inspected per direction when WithLogPayloads is enabled. Values <= 0 are ignored; the built-in fallback (1024) applies.
func (*Builder) WithPeerName ¶ added in v0.37.0
WithPeerName sets a low-cardinality logical service name attached to every metric emitted by this client. Intended for SLO attribution (e.g., "stripe", "visa-vts"). When unset, only the high-cardinality server.address is recorded.
func (*Builder) WithRequestInterceptor ¶
func (b *Builder) WithRequestInterceptor(interceptor RequestInterceptor) *Builder
WithRequestInterceptor adds a request interceptor
func (*Builder) WithResponseInterceptor ¶
func (b *Builder) WithResponseInterceptor(interceptor ResponseInterceptor) *Builder
WithResponseInterceptor adds a response interceptor
func (*Builder) WithRetries ¶
WithRetries sets the retry configuration
func (*Builder) WithTLSConfig ¶ added in v0.55.0
WithTLSConfig fills the base-transport slot: it clones an incumbent *nethttp.Transport when present (or DefaultTransport otherwise) and replaces — never merges — its TLSClientConfig with tlsCfg. Last call between this and WithTransport wins. A nil tlsCfg is a no-op. The clone is shallow: don't mutate tlsCfg's Certificates/RootCAs in place — rotate via GetClientCertificate.
func (*Builder) WithTimeout ¶
WithTimeout sets the request timeout
func (*Builder) WithTraceIDExtractor ¶
WithTraceIDExtractor sets a function to extract a trace ID from context
func (*Builder) WithTraceIDGenerator ¶
WithTraceIDGenerator sets the generator used when no trace ID is present
func (*Builder) WithTraceIDHeader ¶
WithTraceIDHeader sets the header name used for the trace ID (default: X-Request-ID)
func (*Builder) WithTransport ¶
func (b *Builder) WithTransport(transport nethttp.RoundTripper) *Builder
WithTransport sets a custom RoundTripper while still letting the builder manage other client settings. It supplies the innermost transport: wrappers registered by other options (WithJOSE, for example) are layered above it at Build time regardless of call order. Passing nil vacates the base-transport slot; if WithTLSConfig had filled it, Build fails with an error reporting that the loaded TLS material would be discarded — unless the transport passed here is itself a *nethttp.Transport deciding its own TLS (transportCarriesTLSMaterial). The replacement still wins either way; the two configs are never merged, so that case is silent, not composed.
func (*Builder) WithW3CTrace ¶
WithW3CTrace enables or disables W3C trace context propagation
type Client ¶
type Client interface {
Get(ctx context.Context, req *Request) (*Response, error)
Post(ctx context.Context, req *Request) (*Response, error)
Put(ctx context.Context, req *Request) (*Response, error)
Patch(ctx context.Context, req *Request) (*Response, error)
Delete(ctx context.Context, req *Request) (*Response, error)
Do(ctx context.Context, method string, req *Request) (*Response, error)
}
Client defines the REST client interface for making HTTP requests
type ClientError ¶
ClientError represents different types of REST client errors
func NewHTTPError ¶
func NewHTTPError(message string, statusCode int, body []byte) ClientError
NewHTTPError creates a new HTTP error
func NewInterceptorError ¶
func NewInterceptorError(message, stage string, wrapped error) ClientError
NewInterceptorError creates a new interceptor error
func NewNetworkError ¶
func NewNetworkError(message string, wrapped error) ClientError
NewNetworkError creates a new network error
func NewTimeoutError ¶
func NewTimeoutError(message string, timeout time.Duration) ClientError
NewTimeoutError creates a new timeout error
func NewValidationError ¶
func NewValidationError(message, field string) ClientError
NewValidationError creates a new validation error
type ClientTLSConfig ¶ added in v0.55.0
type ClientTLSConfig struct {
CertFile string
CertValue string
KeyFile string
KeyValue string
CAFile string
CAValue string
// ServerName overrides SNI / hostname verification (optional).
ServerName string
// MinVersion is "1.2" (default when empty) or "1.3".
MinVersion string
// RequireClientCert makes a missing client certificate an error instead of
// silently producing a server-authentication-only config. Set it whenever the
// deployment intends mutual TLS: a CA-only config is valid (root pinning) but
// presents no client certificate.
RequireClientCert bool
}
ClientTLSConfig describes client-side TLS material declaratively. Each piece comes from a PEM file path (File) or a base64-encoded PEM string (Value) — set exactly one source per provided piece. Cert and Key must be provided together (the client certificate); CA is optional and, when set, REPLACES the system roots for server verification (private-CA pinning), so a client that pins a private CA can no longer verify public-CA endpoints. Providing no material at all is an error — use WithTLSConfig with a hand-built *tls.Config for setups this does not cover.
Every field is a comparable type on purpose: a slice or map would make the exported struct non-comparable, which apidiff reports as INCOMPATIBLE.
type Config ¶
type Config struct {
Timeout time.Duration
MaxRetries int
RetryDelay time.Duration
RequestInterceptors []RequestInterceptor
ResponseInterceptors []ResponseInterceptor
BasicAuth *BasicAuth
DefaultHeaders map[string]string
// LogPayloads enables debug-level logging of headers and body payloads
LogPayloads bool
// MaxPayloadLogBytes caps the number of body bytes logged when LogPayloads is enabled
MaxPayloadLogBytes int
// TraceIDHeader configures the header name used for trace ID propagation (default: X-Request-ID)
TraceIDHeader string
// NewTraceID generates a new trace ID when none is present (default: uuid)
NewTraceID func() string
// TraceIDExtractor allows advanced extraction of a trace ID from context; return ok=false to fallback to generator
TraceIDExtractor func(_ context.Context) (traceID string, ok bool)
// EnableW3CTrace enables W3C Trace Context (traceparent/tracestate) propagation and generation
EnableW3CTrace bool
// PeerName is a low-cardinality logical service name attached to every metric emitted
// by this client. Intended for SLO attribution (e.g., "stripe", "visa-vts").
// When unset, only the high-cardinality server.address is recorded.
PeerName string
}
Config holds the REST client configuration
type JOSEConfig ¶ added in v0.30.0
type JOSEConfig struct {
// Outbound is optional: when set, it is the policy used to seal every outbound
// request body (sign+encrypt, or encrypt-only under SealModeBareJWE). Nil disables
// outbound sealing, so request bodies go out untouched.
Outbound *jose.Policy
// Inbound is optional: when set, application/jose response bodies are opened
// (decrypt+verify, or decrypt-only under SealModeBareJWE).
// Plaintext responses (e.g., pre-trust error envelopes from the counterparty) pass
// through unmodified.
Inbound *jose.Policy
// Resolver supplies keys for both Outbound and Inbound directions.
Resolver jose.KeyResolver
// Envelope optionally shapes the sealed body on the wire in both directions -- Visa
// MLE's {"encData":...} object, for example, from httpclient.VisaMLEEnvelope(). Nil
// sends the compact itself as application/jose and unwraps only application/jose
// responses. Wrap is consulted only when Outbound is set and Unwrap only when Inbound
// is set, so an Envelope with neither policy fails Build
// (JOSE_POLICY_ENVELOPE_UNPAIRED); either policy alone is a supported shape.
Envelope BodyEnvelope
// MaxResponseBytes bounds the inbound response body read, exactly as the field of the
// same name on JOSETransport: zero means DefaultMaxJOSEBodyBytes and a negative value
// disables the cap. Negative beside an Envelope and an Inbound policy fails Build,
// because Unwrap replaces the Content-Type gate and every response body would then be
// buffered without limit.
MaxResponseBytes int64
}
JOSEConfig groups the JOSE policy and resolver passed to Builder.WithJOSE. A struct is used (rather than positional parameters) so future fields — clock override, replay-cache hook, per-call policy resolver — can be added without changing the WithJOSE signature.
type JOSETransport ¶ added in v0.30.0
type JOSETransport struct {
// Inner is the underlying RoundTripper that performs the actual HTTP exchange.
// Nil defaults to nethttp.DefaultTransport — relevant only when JOSETransport is
// hand-constructed, since httpclient.Builder-produced clients always seed a non-nil Inner.
Inner nethttp.RoundTripper
// Outbound is optional: when set, it is the policy used to seal every outbound
// request body (sign+encrypt, or encrypt-only under SealModeBareJWE).
// A nil Outbound disables outbound wrapping entirely (the transport delegates to Inner).
Outbound *jose.Policy
// Inbound is optional: when set, application/jose responses are opened
// (decrypt+verify, or decrypt-only under SealModeBareJWE).
// Other response Content-Types pass through unmodified so plaintext error envelopes
// from JOSE-aware counterparties (e.g., GoBricks pre-trust failures) remain readable.
Inbound *jose.Policy
// Resolver supplies keys for both Outbound (sign/encrypt) and Inbound (decrypt/verify).
// Required when either policy is set.
Resolver jose.KeyResolver
// MaxResponseBytes bounds the response body read when Inbound is set. Zero means
// use DefaultMaxJOSEBodyBytes. A negative value disables the cap entirely, which is
// only defensible while the application/jose Content-Type gate has already vouched
// for the body — that is, while Envelope is nil.
//
// Negative beside an Envelope and an Inbound policy is refused, not quietly
// defaulted: Builder.WithJOSE rejects it at Build time and RoundTrip rejects it
// before the request is sent, both as JOSE_POLICY_ENVELOPE_UNBOUNDED. A silent
// fallback would hide the misconfiguration instead of naming it.
MaxResponseBytes int64
// Envelope optionally shapes the sealed body on the wire in both directions. Nil is
// the identity: the compact itself outbound, the application/jose Content-Type rule
// inbound. Wrap runs only when Outbound is set, Unwrap only when Inbound is set.
//
// With an Envelope set and Inbound non-nil, EVERY eligible response body is buffered
// before Unwrap decides — bounded by MaxResponseBytes, with the same default and
// over-cap error — because the Content-Type gate that would otherwise leave a
// non-JOSE body unread no longer applies.
Envelope BodyEnvelope
}
JOSETransport is an http.RoundTripper that seals outbound request bodies (jose.Seal) and opens inbound response bodies (jose.Open) using a fixed pair of policies and a single KeyResolver. What sealing and opening MEAN is the policy's Mode: sign+encrypt and decrypt+verify on the nested JWE-of-JWS default, encrypt-only and decrypt-only on a SealModeBareJWE policy, which carries no signature to verify.
Only bodies are protected: a request with no body is forwarded unsealed regardless of method, and a response net/http guarantees is empty (1xx, 204, 304, any reply to HEAD) is returned as-is even when it advertises application/jose. Every other response carrying that content type is decrypted and verified, including shapes that are bodyless by RFC but not by net/http — see unwrapResponse for why the guarantee, not the RFC, sets the boundary.
Architectural placement: JOSETransport sits below the httpclient retry loop, so each retry attempt produces a freshly-sealed request — important for protocols that require unique iat/jti claims per attempt (Visa Token Services and similar).
Response Content-Type discrimination: only application/jose responses are unwrapped; other Content-Types pass through untouched. This mirrors the GoBricks server's hybrid error envelope — pre-trust failures from the counterparty come back as plaintext minimal JSON because the peer was never authenticated, and the transport must not attempt to decrypt those.
An Envelope moves that boundary for counterparties whose protected payload travels inside another format — Visa Message Level Encryption's {"encData":"<compact>"} JSON envelope, for example. Its Unwrap replaces the Content-Type rule, which costs a buffered read of every eligible response body.
func (*JOSETransport) RoundTrip ¶ added in v0.30.0
RoundTrip wraps the request body with JOSE (when Outbound is set), forwards to the inner transport, and unwraps the response body (when Inbound is set AND the response is recognized as protected — by Content-Type, or by Envelope.Unwrap when one is set).
type RequestInterceptor ¶
RequestInterceptor is called before sending the request
func NewTraceIDInterceptor ¶
func NewTraceIDInterceptor() RequestInterceptor
NewTraceIDInterceptor creates a request interceptor that adds trace ID headers This provides an alternative approach for users who want explicit control
func NewTraceIDInterceptorFor ¶
func NewTraceIDInterceptorFor(header string) RequestInterceptor
NewTraceIDInterceptorFor creates an interceptor that uses a custom header name