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 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 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 WithJOSE. 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 signs+encrypts every outbound request body and decrypts+verifies application/jose response bodies. Pass cfg.Inbound = nil when the counterparty does not return JOSE-wrapped responses.
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.
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 required: the policy used to sign+encrypt every outbound request body.
Outbound *jose.Policy
// Inbound is optional: when set, application/jose response bodies are decrypted+verified.
// 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
}
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 required: the policy used to sign+encrypt every outbound request body.
// A nil Outbound disables outbound wrapping entirely (the transport delegates to Inner).
Outbound *jose.Policy
// Inbound is optional: when set, application/jose responses are decrypted+verified.
// 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 (NOT
// recommended for untrusted counterparties).
MaxResponseBytes int64
}
JOSETransport is an http.RoundTripper that signs+encrypts outbound request bodies (jose.Seal) and decrypts+verifies inbound response bodies (jose.Open) using a fixed pair of policies and a single KeyResolver.
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.
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