httpclient

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 13, 2026 License: MIT Imports: 23 Imported by: 0

Documentation

Overview

Package httpclient provides production-oriented outbound HTTP clients with bounded execution, retries, propagation, observability, and optional cached dependency health.

Execution policy

Ordinary requests use DefaultRetryConfig when Config.Retry is zero. Ordinary response classification accepts 2xx by default and may be overridden at the client or individual request level. Accepted responses are never retried; rejected responses remain subject to the configured retry policy. Automatic retries and method-preserving 307/308 redirects also require semantic authorization: standard idempotent methods are authorized by default, while POST, PATCH, CONNECT, and custom methods require an explicit per-operation assertion. Ordinary 301, 302, and 303 redirects retain net/http behavior. The default transport policy retries transient-looking and unknown failures, but fails immediately for recognized TLS failures, DNS not-found, and an invalid no-response/no-error RoundTripper result. Health checks are disabled by default and require both CheckConfig.Enabled and a relative path; DefaultCheckConfig is the usual way to enable them. Health checks have independent timeout and retry policies. A zero health-check retry policy performs one attempt; callers must assign DefaultRetryConfig or another complete policy to enable health retries explicitly.

Custom response classifiers must be concurrency-safe, return quickly, and must not retain responses or read, close, or replace response bodies. Classifier panics and unsupported dispositions become non-retryable policy failures without synthesizing Result.Err.

NewRequest resolves references against BaseURL using RFC 3986 semantics. BaseURL paths are directory resolution bases rather than confinement boundaries: root-relative and parent references may replace or escape them. Clientkit still enforces the configured HTTP(S) origin unless cross-origin execution is explicitly enabled.

Observability and readiness

With the owned default net/http client and a nil Observer, Clientkit emits one INTERNAL span for the complete logical operation and one CLIENT span for each transport RoundTrip, including redirects and retries. Trace context is injected from each CLIENT span. Logical and physical spans end when response headers or a terminal error are available; they do not depend on callers reading or closing response bodies. Clientkit's execution contexts remain attached to a returned body until it completes, is closed, or its context is done, so callers should still close bodies promptly.

A caller-supplied HTTPClient is never mutated or automatically instrumented. Likewise, a non-nil Observer completely replaces automatic observation. Use otel.NewTransport on a caller-owned transport when physical OpenTelemetry spans are wanted with either of those configurations. This explicit boundary avoids duplicate spans when the supplied transport is already instrumented. NopObserver and NopHeaderPropagator disable their respective behavior, while the Multi variants compose behavior explicitly.

Global OpenTelemetry providers and the global text-map propagator are selected when an observer, transport, or Client is constructed, so applications should configure them first. Applications own the OpenTelemetry SDK and exporter lifecycle. Registry readiness reads cached health and never performs synchronous dependency calls. HTTP clients report the stable protocol category "http" for both HTTP and HTTPS endpoints. Clientkit-controlled telemetry and operational snapshots exclude paths, URLs, headers, and bodies. The default OpenTelemetry observer also omits raw operation errors; an explicitly configured otel.Observer may opt into them with otel.WithErrorDetails. Standard HTTP request-duration metrics and request-target span attributes require explicit transport options because their server and path dimensions may be sensitive or unsuitable for low-cardinality defaults.

Context metadata

Context-derived request metadata can be injected through HeaderPropagator without defining context keys or generating identifiers. Existing headers are preserved and values are limited to 256 bytes by default. Invalid values and provider panics omit only the affected header and never expose metadata through telemetry.

Configuration examples

The examples below use these module imports:

import (
	"context"
	"net/http"
	"time"

	clientkit "github.com/jaredjakacky/clientkit"
	httpclient "github.com/jaredjakacky/clientkit/httpclient"
	httpclientotel "github.com/jaredjakacky/clientkit/httpclient/otel"
)

Application-owned context accessors can supply request and correlation IDs. Header names remain configurable and identifiers are not generated by Clientkit:

metadataHeaders, err := httpclient.NewRequestMetadataPropagator(
	httpclient.RequestMetadataConfig{
		RequestID: httpclient.HeaderValueProviderFunc(
			func(ctx context.Context) (string, bool) {
				return applicationRequestID(ctx)
			},
		),
		RequestIDHeader: "Request-Id",
		CorrelationID: httpclient.HeaderValueProviderFunc(
			func(ctx context.Context) (string, bool) {
				return applicationCorrelationID(ctx)
			},
		),
	},
)
if err != nil {
	// handle error
}

The metadata propagator can be explicitly composed with OpenTelemetry trace propagation:

metadataClient, err := httpclient.New(httpclient.Config{
	Config: clientkit.Config{
		Name: "payments",
	},
	BaseURL: "https://payments.internal",
	Propagator: httpclient.MultiHeaderPropagator(
		httpclientotel.New(),
		metadataHeaders,
	),
})

A client-level classifier can accept 404 as a normal catalog result:

classifier, err := httpclient.AcceptAnyStatus(
	http.StatusOK,
	http.StatusNotFound,
)
if err != nil {
	// handle error
}

client, err := httpclient.New(httpclient.Config{
	Config: clientkit.Config{
		Name: "catalog",
	},
	BaseURL:           "https://catalog.internal",
	ResponseClassifier: classifier,
})

An individual request can completely override the client classifier:

classifier, err := httpclient.AcceptStatus(http.StatusNotModified)
if err != nil {
	// handle error
}

result := client.ExecuteWithOptions(request, httpclient.ExecuteOptions{
	ResponseClassifier: classifier,
})

Applications can declare a stable, bounded vocabulary for logical operations and attach one name to an ordinary execution:

const (
	operationCreatePayment httpclient.OperationName = "payments.create"
	operationLookupPayment httpclient.OperationName = "payments.lookup"
)

result := client.ExecuteWithOptions(request, httpclient.ExecuteOptions{
	Operation: operationCreatePayment,
})

A caller can explicitly assert that one POST operation is idempotent. Any idempotency key remains application-owned:

request, err := client.NewRequest(
	context.Background(),
	http.MethodPost,
	"/payments",
	body,
)
if err != nil {
	// handle error
}

request.Header.Set("Idempotency-Key", key)

result := client.ExecuteWithOptions(request, httpclient.ExecuteOptions{
	RetrySafety: httpclient.RetrySafetyIdempotent,
})

Automatic retries can be disabled for one operation without changing the shared client:

result := client.ExecuteWithOptions(request, httpclient.ExecuteOptions{
	Retry: httpclient.ExecutionRetry{
		Disable: true,
	},
})

A complete custom policy can narrow one operation to two attempts:

retry := httpclient.DefaultRetryConfig()
retry.MaxAttempts = 2

result := client.ExecuteWithOptions(request, httpclient.ExecuteOptions{
	Retry: httpclient.ExecutionRetry{
		Config: retry,
	},
})

A semantic name composes with custom retry, retry-safety, and timeout policy:

retry = httpclient.DefaultRetryConfig()
retry.MaxAttempts = 2
retry.Methods = append(retry.Methods, http.MethodPost)

result = client.ExecuteWithOptions(request, httpclient.ExecuteOptions{
	Operation: operationCreatePayment,
	Retry: httpclient.ExecutionRetry{
		Config: retry,
	},
	RetrySafety: httpclient.RetrySafetyIdempotent,
	Timeouts: httpclient.ExecutionTimeouts{
		Timeout: 2 * time.Second,
	},
})

Total and per-attempt budgets can be shortened independently for one operation without mutating the shared client:

result := client.ExecuteWithOptions(request, httpclient.ExecuteOptions{
	Timeouts: httpclient.ExecutionTimeouts{
		Timeout:        2 * time.Second,
		AttemptTimeout: 750 * time.Millisecond,
	},
})

A Clientkit total timeout can be disabled while retaining the caller's deadline. In this example, the five-minute deadline remains active:

ctx, cancel := context.WithTimeout(
	context.Background(),
	5*time.Minute,
)
defer cancel()

request, err := client.NewRequest(
	ctx,
	http.MethodGet,
	"/export",
	nil,
)
if err != nil {
	// handle error
}

result := client.ExecuteWithOptions(request, httpclient.ExecuteOptions{
	Timeouts: httpclient.ExecutionTimeouts{
		DisableTimeout: true,
	},
})

A typical enabled check is configured as follows:

check := httpclient.DefaultCheckConfig("/healthz")

client, err := httpclient.New(httpclient.Config{
	Config: clientkit.Config{
		Name: "payments",
	},
	BaseURL: "https://payments.internal",
	Check:   check,
})

A status-class classifier can replace the default exact status:

check := httpclient.DefaultCheckConfig("/ready")
check.ResponseClassifier, err = httpclient.AcceptStatusClass(2)
if err != nil {
	// handle error
}

A non-idempotent health-check method also requires explicit authorization in addition to inclusion in its complete retry policy:

check := httpclient.DefaultCheckConfig("/ready")
check.Method = http.MethodPost
check.Retry = httpclient.DefaultRetryConfig()
check.Retry.Methods = append(check.Retry.Methods, http.MethodPost)
check.RetrySafety = httpclient.RetrySafetyIdempotent

Index

Constants

View Source
const (
	// DefaultCheckMethod is the default method for enabled HTTP health checks.
	DefaultCheckMethod = http.MethodGet
	// DefaultCheckTimeout is the default outer timeout for an HTTP health check.
	DefaultCheckTimeout = 5 * time.Second
	// DefaultCheckStaleAfter is the default age after which cached HTTP health is
	// stale.
	DefaultCheckStaleAfter = 90 * time.Second
	// DefaultCheckStatus is the default exact HTTP health-check status.
	DefaultCheckStatus = http.StatusOK
)
View Source
const (
	// DefaultContextHeaderMaxValueBytes is the default byte limit for one
	// context-derived header value.
	DefaultContextHeaderMaxValueBytes = 256
	// DefaultRequestIDHeader is a common request-ID header convention, not an
	// IETF standard.
	DefaultRequestIDHeader = "X-Request-ID"
	// DefaultCorrelationIDHeader is a common correlation-ID header convention,
	// not an IETF standard.
	DefaultCorrelationIDHeader = "X-Correlation-ID"
)
View Source
const (
	// DefaultTimeout limits Clientkit request execution and final response-body use.
	DefaultTimeout = 30 * time.Second
	// DefaultAttemptTimeout limits one Clientkit execution attempt and final
	// response-body use. Redirects can cause multiple RoundTrips in one attempt.
	DefaultAttemptTimeout = 10 * time.Second
	// DefaultDialTimeout limits connection establishment.
	DefaultDialTimeout = 5 * time.Second
	// DefaultDialKeepAlive controls TCP keep-alive probes.
	DefaultDialKeepAlive = 30 * time.Second
	// DefaultMaxIdleConns limits idle connections across all hosts.
	DefaultMaxIdleConns = 100
	// DefaultMaxIdleConnsPerHost limits idle connections retained per host.
	DefaultMaxIdleConnsPerHost = 20
	// DefaultMaxConnsPerHost bounds active and idle connections per host.
	DefaultMaxConnsPerHost = 100
	// DefaultIdleConnTimeout limits how long idle connections remain pooled.
	DefaultIdleConnTimeout = 90 * time.Second
	// DefaultTLSHandshakeTimeout limits TLS handshakes.
	DefaultTLSHandshakeTimeout = 10 * time.Second
	// DefaultResponseHeaderTimeout limits waiting for response headers.
	DefaultResponseHeaderTimeout = 10 * time.Second
	// DefaultExpectContinueTimeout limits waiting for a 100-continue response.
	DefaultExpectContinueTimeout = 1 * time.Second
	// DefaultRetryMaxAttempts is the total number of attempts, including the initial request.
	DefaultRetryMaxAttempts = 3
	// DefaultRetryBackoff is the delay before the first retry.
	DefaultRetryBackoff = 200 * time.Millisecond
	// DefaultRetryBackoffMultiplier controls exponential retry-delay growth.
	DefaultRetryBackoffMultiplier = 2.0
	// DefaultRetryMaxBackoff caps retry delays.
	DefaultRetryMaxBackoff = 2 * time.Second
	// DefaultRetryJitter bounds random variation applied to retry delays.
	DefaultRetryJitter = 100 * time.Millisecond
	// DefaultRespectRetryAfter enables bounded server-directed retry timing for
	// responses already retryable under the configured policy.
	DefaultRespectRetryAfter = true
	// DefaultMaxRetryAfter caps the server-requested portion of a retry delay.
	DefaultMaxRetryAfter = 30 * time.Second
)
View Source
const MaxOperationNameBytes = 64

MaxOperationNameBytes is the maximum encoded length of an OperationName.

View Source
const (
	// OperationHTTPRequest identifies an outbound HTTP request operation.
	OperationHTTPRequest = "request"
)
View Source
const ProtocolHTTP = "http"

ProtocolHTTP identifies the HTTP client family in registry inspection and observer events.

Variables

This section is empty.

Functions

func DefaultHTTPClient

func DefaultHTTPClient() *http.Client

DefaultHTTPClient returns a new HTTP client using DefaultTransport. Its Timeout remains zero because Clientkit applies operation timeouts with contexts.

func DefaultTransport

func DefaultTransport() *http.Transport

DefaultTransport returns a new production-oriented HTTP transport. Each call returns an independently configurable transport.

Types

type Attempt

type Attempt struct {
	// Number is the one-based execution-attempt number.
	Number int
	// Outcome is the bounded result of this attempt.
	Outcome Outcome
	// FailureClass is the stable classified attempt failure and supplements Err
	// and Outcome without replacing either.
	FailureClass clientkit.FailureClass
	// StatusCode is the received HTTP status, or zero when no response arrived.
	StatusCode int
	// StartedAt is the time at which transport execution began.
	StartedAt time.Time
	// Duration covers transport execution through response headers or error.
	Duration time.Duration
	// Err is the original request or transport execution error.
	Err error
}

Attempt describes one Clientkit HTTP execution attempt. Redirects may cause more than one transport RoundTrip within one execution attempt.

type CheckConfig

type CheckConfig struct {
	// Enabled allows direct and registry-driven health checking.
	Enabled bool

	// Method is the health-check request method.
	Method string
	// Path is the required relative health-check URL reference. It uses the same
	// RFC 3986 BaseURL resolution semantics as NewRequest.
	Path string

	// Timeout bounds the complete health-check execution.
	Timeout time.Duration
	// DisableTimeout disables the health-check outer timeout.
	DisableTimeout bool

	// StaleAfter controls when cached check health is projected as unknown. It
	// should exceed the maximum expected completion-to-completion refresh gap,
	// including scheduler wait, check-group execution and queueing, positive
	// jitter, and scheduler delay.
	StaleAfter time.Duration
	// DisableStaleAfter disables cached-health staleness projection.
	DisableStaleAfter bool

	// ResponseClassifier defines a healthy response. Nil accepts exactly HTTP
	// 200. The same classifier abstraction and panic containment used by ordinary
	// operations applies to health checks.
	ResponseClassifier ResponseClassifier

	// Retry supplies the independent health-check retry policy. Its zero value
	// performs one attempt with no automatic retries. Assign DefaultRetryConfig or
	// another complete non-zero policy to enable retries explicitly. Retries
	// consume the health-check timeout and may delay unhealthy results.
	Retry RetryConfig
	// RetrySafety controls semantic authorization for check retries and
	// method-preserving 307/308 redirects. Retry.Methods remains independent for
	// scheduled retries, and body replayability remains a mechanical requirement.
	// POST, PATCH, CONNECT, and custom methods require RetrySafetyIdempotent, which
	// is a caller assertion rather than a Clientkit guarantee. RetrySafetyNever
	// disables check retries and rejects 307/308 redirects.
	RetrySafety RetrySafety
}

CheckConfig configures an explicit HTTP health check. Its zero value disables checking. Set Enabled or use DefaultCheckConfig to enable it.

func DefaultCheckConfig

func DefaultCheckConfig(path string) CheckConfig

DefaultCheckConfig returns an enabled, independently mutable health-check configuration with production-safe defaults and no automatic retries.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client executes HTTP requests and optionally maintains cached dependency health through explicitly enabled checks.

func New

func New(cfg Config) (*Client, error)

New validates and constructs an HTTP client without performing network I/O. Nil HTTPClient, Observer, Propagator, and ResponseClassifier fields select their documented production defaults. Health checks remain disabled unless Check.Enabled is true.

func (*Client) Check

func (c *Client) Check(ctx context.Context) clientkit.Health

Check executes one enabled HTTP health check using its independent timeout and retry policy. Disabled checks return unknown without network I/O, cache mutation, or health telemetry.

func (*Client) CloseIdleConnections

func (c *Client) CloseIdleConnections()

CloseIdleConnections synchronously closes currently idle connections held by the configured HTTP client. It does not cancel or wait for active requests, permanently close this Client, or prevent future requests and health checks from opening new connections. A nil or unusable Client is a no-op.

This explicit call also applies to a caller-supplied HTTP client. If that client or its transport is shared, other users of the same idle pool may be affected. Clientkit neither detects that sharing nor claims ownership, and it never performs this cleanup automatically. Applications own active-work draining and shutdown ordering.

Individual cleanup can be requested directly:

payments.CloseIdleConnections()

func (*Client) Do

func (c *Client) Do(request *http.Request) (*http.Response, error)

Do executes request using the client's ordinary policy and returns standard net/http response/error semantics. HTTP status rejection is represented by a non-nil response and nil error. The caller owns any response body and must close it; operation observation completes when final response headers or a terminal execution error are available.

func (*Client) Execute

func (c *Client) Execute(request *http.Request) Result

Execute executes request and returns Clientkit's detailed classified result. It is equivalent to ExecuteWithOptions with zero-value options.

func (*Client) ExecuteWithOptions

func (c *Client) ExecuteWithOptions(request *http.Request, options ExecuteOptions) Result

ExecuteWithOptions executes request with explicit per-request policy. A non-nil options classifier completely overrides the client classifier for this call. Accepted responses are never retried; rejected responses may retry under the selected retry policy only when RetrySafety authorizes repetition. RetrySafety also authorizes method-preserving 307/308 redirects, while 301, 302, and 303 retain ordinary net/http behavior. RetrySafetyNever disables retries and rejects 307/308 redirects for this call. Result.Err reports setup, request, and transport execution errors, and the caller owns any final response body. ExecuteWithOptions takes ownership of a non-nil request body and closes it even when validation prevents the first execution attempt.

func (*Client) Health

func (c *Client) Health() clientkit.Health

Health returns cached health with read-time staleness projection for enabled checks. It never performs network I/O or mutates cached health.

func (*Client) HealthCheckEnabled

func (c *Client) HealthCheckEnabled() bool

HealthCheckEnabled reports whether active HTTP health checking is enabled.

func (*Client) Name

func (c *Client) Name() string

Name returns the client's immutable logical name.

func (*Client) NewRequest

func (c *Client) NewRequest(ctx context.Context, method string, path string, body io.Reader) (*http.Request, error)

NewRequest resolves a relative URL reference against the configured BaseURL using RFC 3986 semantics and returns a request bound to ctx. Root-relative and parent references may replace or escape the BaseURL path, which is not a confinement boundary. Absolute references and fragments are rejected so endpoint-origin policy remains explicit.

func (*Client) Propagator

func (c *Client) Propagator() HeaderPropagator

Propagator returns the client's concurrency-safe outbound header propagator. A nil or unusable client returns NopHeaderPropagator.

func (*Client) Protocol

func (c *Client) Protocol() string

Protocol returns the client's stable HTTP family identity.

func (*Client) ReadinessPolicy

func (c *Client) ReadinessPolicy() clientkit.ReadinessPolicy

ReadinessPolicy returns the client's immutable normalized readiness policy.

func (*Client) ResponseClassifier

func (c *Client) ResponseClassifier() ResponseClassifier

ResponseClassifier returns the client's immutable panic-safe ordinary HTTP response classifier. A nil or unusable client returns the default 2xx classifier.

func (*Client) Snapshot

func (c *Client) Snapshot() clientkit.ClientSnapshot

Snapshot returns the client's identity, readiness policy, and effective cached health. It never performs a synchronous dependency check.

type Config

type Config struct {
	// Config supplies shared Clientkit identity, readiness, and observation.
	Config clientkit.Config
	// BaseURL is the required endpoint origin and optional directory path used as
	// NewRequest's RFC 3986 resolution base. Root-relative and parent references
	// may replace or escape that path; it is not a path-confinement boundary.
	BaseURL string
	// HTTPClient replaces the default net/http client when non-nil. Clientkit
	// still applies its contexts and request-origin policy and does not mutate or
	// claim ownership of the supplied client. Its transport is not automatically
	// instrumented; callers can wrap it explicitly with httpclient/otel when
	// physical HTTP spans or standard HTTP metrics are wanted.
	// Client.CloseIdleConnections uses this client directly; if its transport is
	// shared, that call may affect other users of the same idle pool.
	HTTPClient *http.Client
	// Propagator completely replaces the default OpenTelemetry trace propagator
	// when non-nil. Use NopHeaderPropagator to disable propagation or
	// MultiHeaderPropagator to compose propagators explicitly.
	Propagator HeaderPropagator
	// AllowCrossOrigin permits HTTP or HTTPS requests and redirects whose scheme,
	// host, or port differ from BaseURL. This can forward caller-supplied headers
	// to another origin or permit an HTTPS downgrade; the production default
	// rejects it. Non-HTTP URL schemes are always rejected.
	// Pair it with a restrictive CheckRedirect policy when redirects are enabled.
	AllowCrossOrigin bool
	// AllowHostOverride permits Request.Host to differ from Request.URL.Host. The
	// production default rejects host overrides.
	AllowHostOverride bool
	// ResponseClassifier defines ordinary HTTP operation success. Nil accepts
	// 2xx responses through DefaultResponseClassifier. Health checks do not use
	// this policy.
	ResponseClassifier ResponseClassifier
	// Timeout bounds request execution, including retries and retry delays, and
	// remains active for final response-body use. Logical observation and Result
	// duration stop at final response headers or a terminal error. Zero selects
	// DefaultTimeout.
	Timeout time.Duration
	// DisableTimeout intentionally disables Clientkit's total execution timeout.
	DisableTimeout bool
	// AttemptTimeout bounds each Clientkit execution attempt and remains active
	// for final response-body use. One execution attempt may contain multiple
	// RoundTrips because of redirects. Zero selects DefaultAttemptTimeout.
	AttemptTimeout time.Duration
	// DisableAttemptTimeout intentionally disables Clientkit's per-attempt timeout.
	DisableAttemptTimeout bool
	// Check configures explicit health checking. Its zero value is disabled.
	Check CheckConfig
	// Retry configures ordinary requests. Its zero value selects
	// DefaultRetryConfig, including classified transport-error behavior, and does
	// not affect health-check retries.
	Retry RetryConfig
}

Config defines an HTTP client and its independent ordinary-request and health-check policies.

type ContextHeaderBinding

type ContextHeaderBinding struct {
	// Header is the required HTTP field name. It must not contain surrounding
	// whitespace and is stored in canonical MIME form.
	Header string
	// Provider reads the value from the attempt context.
	Provider HeaderValueProvider
	// ExistingPolicy preserves an existing header by default. Overwriting must
	// be selected explicitly with OverwriteExistingHeader.
	ExistingPolicy ExistingHeaderPolicy
	// MaxValueBytes overrides DefaultContextHeaderMaxValueBytes when positive.
	MaxValueBytes int
	// DisableValueLimit disables Clientkit's byte limit. It cannot be combined
	// with a positive MaxValueBytes.
	DisableValueLimit bool
}

ContextHeaderBinding configures one context-derived outbound HTTP header. Values are limited to DefaultContextHeaderMaxValueBytes unless overridden or explicitly disabled. Invalid dynamic values are silently omitted. Clientkit does not define context keys, generate values, or expose values through telemetry, health, inspection, snapshots, or errors. Custom header propagation may have security consequences, which remain the caller's responsibility.

type ExecuteOptions

type ExecuteOptions struct {
	// Operation supplies a stable, low-cardinality semantic name for this logical
	// execution. Zero uses OperationHTTPRequest. Custom names use the restricted
	// lowercase OperationName syntax and appear in spans, metrics, retry events,
	// and structured logs. They do not affect execution, are not sent remotely,
	// and must never be derived from URLs, paths, IDs, or user input. The value is
	// resolved per call without mutating Client and is safe for concurrent use.
	Operation OperationName
	// ResponseClassifier overrides Config.ResponseClassifier for this operation.
	// Nil uses the immutable client-level classifier.
	ResponseClassifier ResponseClassifier
	// Retry overrides the Client retry policy for this operation. Its zero value
	// inherits the immutable client policy. Config is a complete replacement,
	// while Disable performs one attempt without scheduling retries. RetrySafety
	// and request-body replayability remain independent requirements. Overrides
	// are resolved per call without mutating Client, and health-check retry policy
	// is unaffected.
	Retry ExecutionRetry
	// RetrySafety controls whether repeating this operation through a scheduled
	// retry or method-preserving 307/308 redirect is semantically authorized.
	// RetryConfig.Methods remains an independent requirement for scheduled
	// retries, while body replayability remains a mechanical requirement. POST,
	// PATCH, CONNECT, and custom methods require RetrySafetyIdempotent, which is a
	// caller assertion rather than a Clientkit guarantee. RetrySafetyNever
	// disables retries and rejects 307/308 redirects for this operation.
	RetrySafety RetrySafety
	// Timeouts overrides the client's total and per-attempt timeout policy
	// field-by-field. Zero fields inherit client values. The total timeout spans
	// retries and delays; the attempt timeout restarts for every attempt and
	// continues through final response-body use. Disable flags remove the
	// corresponding Clientkit timeout without detaching caller or observer-derived
	// deadlines, and the earliest context deadline wins. Per-operation values do
	// not mutate Client; final response bodies must still be closed.
	Timeouts ExecutionTimeouts
}

ExecuteOptions supplies explicit per-request response, retry, repetition-safety, and timeout policy. A non-nil ResponseClassifier completely overrides the client classifier for this call; a zero ExecutionRetry inherits the client retry policy, RetrySafetyDefault uses built-in HTTP method semantics for retries and 307/308 redirects, and zero timeout fields inherit client-level values.

type ExecutionRetry

type ExecutionRetry struct {
	// Config completely replaces the Client retry policy for this operation.
	// Its zero value means no custom policy was supplied. The configuration is
	// normalized and its slices are cloned before execution begins.
	Config RetryConfig
	// Disable performs one Clientkit execution attempt and schedules no automatic
	// retries. RetrySafety separately controls 307/308 redirects; ordinary 301,
	// 302, and 303 redirects may still cause multiple transport RoundTrips. Disable
	// cannot be combined with Config.
	Disable bool
}

ExecutionRetry supplies a retry-policy override for one logical HTTP operation. Its zero value inherits the Client's normalized retry policy. Config is a complete replacement, while Disable performs the initial attempt without scheduling retries; Config and Disable cannot be combined.

Per-operation policies are normalized before execution and never mutate the Client, making concurrent calls with different policies safe. RetrySafety remains an independent semantic gate, request-body replayability remains required, and Retry-After behavior comes from the selected policy. The total operation timeout remains authoritative, and health-check retry policy is unaffected.

type ExecutionTimeouts

type ExecutionTimeouts struct {
	// Timeout overrides the total logical-operation timeout when positive. Zero
	// inherits the client value. The total timeout spans attempts, retry delays,
	// Retry-After delays, and final response-body use. Negative values are
	// invalid, and a positive value cannot be combined with DisableTimeout.
	Timeout time.Duration
	// DisableTimeout removes Clientkit's total timeout for this operation. Caller
	// and observer-derived cancellation and deadlines remain authoritative.
	DisableTimeout bool
	// AttemptTimeout overrides the timeout independently applied to every
	// Clientkit execution attempt, including redirects and final response-body
	// use, when positive. Zero inherits the client value, and a fresh attempt
	// timeout begins for every retry. Negative values are invalid, and a positive
	// value cannot be combined with DisableAttemptTimeout.
	AttemptTimeout time.Duration
	// DisableAttemptTimeout removes Clientkit's per-attempt timeout for this
	// operation without disabling its total, caller, or observer-derived context.
	DisableAttemptTimeout bool
}

ExecutionTimeouts supplies field-by-field timeout overrides for one logical HTTP operation. Zero values inherit the client's normalized policy. Positive values replace one corresponding timeout, while disable flags remove that Clientkit timeout without detaching caller or observer-derived contexts. The earliest context deadline wins naturally. Overrides are immutable per call, safe for concurrent use, and do not mutate Client or http.Client. Final response bodies must still be closed by callers.

type ExistingHeaderPolicy

type ExistingHeaderPolicy string

ExistingHeaderPolicy controls how context-derived metadata interacts with a header already present on an attempt-specific request.

const (
	// PreserveExistingHeader leaves every explicitly present header unchanged,
	// including headers with an empty slice or empty value. It is the default.
	PreserveExistingHeader ExistingHeaderPolicy = ""
	// OverwriteExistingHeader replaces all existing values using Header.Set.
	OverwriteExistingHeader ExistingHeaderPolicy = "overwrite"
)

type HeaderPropagator

type HeaderPropagator interface {
	Inject(context.Context, http.Header)
}

HeaderPropagator injects context-derived metadata into a request-specific outbound header map once per transport RoundTrip. Implementations may be called concurrently, must return quickly, and must not retain or mutate the supplied header after Inject returns. Use Header.Set for single-valued metadata. Header values may contain sensitive data and must never be copied into telemetry attributes.

func MultiHeaderPropagator

func MultiHeaderPropagator(propagators ...HeaderPropagator) HeaderPropagator

MultiHeaderPropagator explicitly invokes non-nil propagators in registration order against the same attempt-specific header map. A panic from one propagator does not prevent later propagators from running.

func NewContextHeaderPropagator

func NewContextHeaderPropagator(bindings ...ContextHeaderBinding) (HeaderPropagator, error)

NewContextHeaderPropagator validates immutable context-derived header bindings. Existing headers are preserved and values are limited to 256 bytes by default. Providers are invoked independently once per RoundTrip; panics and unusable values omit only that binding and never fail the request.

func NewRequestMetadataPropagator

func NewRequestMetadataPropagator(cfg RequestMetadataConfig) (HeaderPropagator, error)

NewRequestMetadataPropagator constructs request-ID and correlation-ID propagation through NewContextHeaderPropagator. Empty enabled header names use the documented conventions. A zero configuration returns a no-op propagator, and explicit names without corresponding providers are invalid.

func SafeHeaderPropagator

func SafeHeaderPropagator(propagator HeaderPropagator) HeaderPropagator

SafeHeaderPropagator prevents propagation panics from affecting HTTP requests. If injection panics, the attempt headers are restored to their prior state. A nil propagator becomes NopHeaderPropagator.

type HeaderPropagatorFunc

type HeaderPropagatorFunc func(context.Context, http.Header)

HeaderPropagatorFunc adapts a function to HeaderPropagator.

func (HeaderPropagatorFunc) Inject

func (fn HeaderPropagatorFunc) Inject(ctx context.Context, headers http.Header)

Inject invokes fn when it is non-nil.

type HeaderValueProvider

type HeaderValueProvider interface {
	// Value returns a context-derived value and whether it is available.
	Value(context.Context) (string, bool)
}

HeaderValueProvider reads one outbound header value from context. Providers are called once for each transport RoundTrip and may be called concurrently. They must be concurrency-safe, return quickly, avoid indefinite blocking, and not retain the context. Providers should return a stable value already stored in context rather than generating a new identifier per call.

type HeaderValueProviderFunc

type HeaderValueProviderFunc func(context.Context) (string, bool)

HeaderValueProviderFunc adapts a function to HeaderValueProvider.

func (HeaderValueProviderFunc) Value

Value invokes fn. A nil function reports no available value.

type NopHeaderPropagator

type NopHeaderPropagator struct{}

NopHeaderPropagator performs no outbound header injection. Supplying it in Config explicitly disables the default OpenTelemetry trace propagator.

func (NopHeaderPropagator) Inject

Inject performs no work.

type OperationName

type OperationName string

OperationName is a stable, low-cardinality application-defined identifier for one logical outbound HTTP operation, such as "payments.create", "payments.lookup", or "catalog.search". Its zero value uses the generic OperationHTTPRequest name.

Custom names must use a fixed vocabulary declared in application source. They must start with a lowercase ASCII letter, end with a lowercase ASCII letter or digit, and contain only lowercase ASCII letters, digits, periods, underscores, and hyphens. URLs, paths, query parameters, user or tenant IDs, request or correlation IDs, random values, dynamic resource identifiers, and other user or unbounded input are prohibited even when they fit the syntax.

The name appears in operation, attempt, and retry telemetry, including spans, metrics, and structured logs. It does not affect execution or retry behavior, is not sent to the remote service, does not mutate Client, and is safe to vary across concurrent operations.

type Outcome

type Outcome string

Outcome classifies one HTTP operation using bounded values.

const (
	// OutcomeSuccess indicates an accepted response.
	OutcomeSuccess Outcome = "success"
	// OutcomeResponseRejected indicates that a completed response was rejected
	// by the configured response classifier.
	OutcomeResponseRejected Outcome = "response_rejected"
	// OutcomeTimeout indicates that request execution timed out.
	OutcomeTimeout Outcome = "timeout"
	// OutcomeCanceled indicates that request execution was canceled.
	OutcomeCanceled Outcome = "canceled"
	// OutcomeExecutionError indicates another request-execution failure.
	// FailureClass identifies whether configuration, request, policy, or
	// transport behavior caused the failure.
	OutcomeExecutionError Outcome = "execution_error"
)

type RequestMetadataConfig

type RequestMetadataConfig struct {
	// RequestID provides the request ID when available.
	RequestID HeaderValueProvider
	// RequestIDHeader overrides DefaultRequestIDHeader. It requires RequestID.
	RequestIDHeader string
	// CorrelationID provides the correlation ID when available.
	CorrelationID HeaderValueProvider
	// CorrelationIDHeader overrides DefaultCorrelationIDHeader. It requires
	// CorrelationID.
	CorrelationIDHeader string
	// ExistingPolicy applies to both metadata headers and preserves existing
	// values by default.
	ExistingPolicy ExistingHeaderPolicy
	// MaxValueBytes applies one shared positive byte limit to both values. Zero
	// selects DefaultContextHeaderMaxValueBytes.
	MaxValueBytes int
	// DisableValueLimit disables Clientkit's value limit for both headers. It
	// cannot be combined with a positive MaxValueBytes.
	DisableValueLimit bool
}

RequestMetadataConfig configures conventional request-ID and correlation-ID propagation from application-owned context values. Header names are configurable; Clientkit defines no context keys, generates no identifiers, and emits no propagated values through telemetry.

type ResponseClassifier

type ResponseClassifier interface {
	// Classify returns whether response is accepted or rejected.
	Classify(*http.Response) ResponseDisposition
}

ResponseClassifier classifies completed HTTP responses. Implementations may be called concurrently and must return quickly. They must not retain the response, read, close, or replace its body, or emit response metadata through Clientkit telemetry attributes. They may inspect bounded metadata such as the status code and headers when application policy requires it.

func AcceptAnyStatus

func AcceptAnyStatus(statusCodes ...int) (ResponseClassifier, error)

AcceptAnyStatus returns an immutable classifier accepting any supplied status code. It clones the supplied values and rejects empty, invalid, or duplicate status sets.

func AcceptStatus

func AcceptStatus(statusCode int) (ResponseClassifier, error)

AcceptStatus returns an immutable classifier accepting exactly statusCode.

func AcceptStatusClass

func AcceptStatusClass(class int) (ResponseClassifier, error)

AcceptStatusClass returns an immutable classifier accepting HTTP status class 1 through 5. Class 2 accepts status codes 200 through 299.

func AcceptStatusRange

func AcceptStatusRange(minimum, maximum int) (ResponseClassifier, error)

AcceptStatusRange returns an immutable classifier accepting every status from minimum through maximum, inclusive.

func DefaultResponseClassifier

func DefaultResponseClassifier() ResponseClassifier

DefaultResponseClassifier returns an immutable classifier that accepts HTTP status codes from 200 through 299 and rejects every other status.

func SafeResponseClassifier

func SafeResponseClassifier(classifier ResponseClassifier) ResponseClassifier

SafeResponseClassifier contains classifier panics and unsupported results. Nil selects DefaultResponseClassifier. During Client execution, either condition becomes OutcomeExecutionError with FailurePolicy and is never retried.

type ResponseClassifierFunc

type ResponseClassifierFunc func(*http.Response) ResponseDisposition

ResponseClassifierFunc adapts a function to ResponseClassifier.

func (ResponseClassifierFunc) Classify

Classify invokes fn. A nil function rejects the response.

type ResponseDisposition

type ResponseDisposition string

ResponseDisposition is Clientkit's closed classification vocabulary for a completed HTTP response. Classification never consumes, decodes, or closes the response; application code continues to own the final response.

const (
	// ResponseAccepted means Clientkit treats the completed response as a
	// successful HTTP operation.
	ResponseAccepted ResponseDisposition = "accepted"
	// ResponseRejected means Clientkit treats the completed response as an HTTP
	// error outcome.
	ResponseRejected ResponseDisposition = "rejected"
)

type Result

type Result struct {
	// Outcome is the bounded final operation result.
	Outcome Outcome
	// FailureClass is the stable classified operation failure and supplements
	// Err and Outcome without replacing either.
	FailureClass clientkit.FailureClass
	// StatusCode is the final response status, or zero when no response exists.
	StatusCode int
	// Response is the final caller-owned response, when one exists. The caller
	// must close any open body. When Err reports redirect-policy rejection,
	// net/http returns the redirect response with its body already closed.
	Response *http.Response
	// StartedAt is the operation start time.
	StartedAt time.Time
	// Duration covers execution through final response headers or terminal error.
	Duration time.Duration
	// Attempts contains one entry for each Clientkit execution attempt.
	Attempts []Attempt
	// Err is the original setup, request, or transport execution error. Response
	// rejection and classifier policy failures do not synthesize errors.
	Err error
}

Result describes one completed HTTP operation. A final Response and its body remain caller-owned. Err reports setup, request, and transport execution failures; response rejection and classifier policy failures use Outcome and FailureClass without synthesizing an error.

type RetryConfig

type RetryConfig struct {
	// MaxAttempts is the total attempt limit, including the initial request.
	MaxAttempts int
	// Backoff is the base delay before the first retry.
	Backoff time.Duration
	// BackoffMultiplier controls exponential delay growth between retries.
	BackoffMultiplier float64
	// MaxBackoff caps the policy delay after backoff and jitter are applied.
	MaxBackoff time.Duration
	// Jitter bounds the random positive or negative delay adjustment.
	Jitter time.Duration
	// StatusCodes lists response statuses eligible for retry.
	StatusCodes []int
	// Methods lists exact request methods eligible for retry. RetrySafety and
	// body replayability remain independent gates.
	Methods []string
	// TransportErrors controls retries for non-timeout transport failures.
	// Its zero value disables them. DefaultRetryConfig selects
	// TransportRetryDefault.
	TransportErrors TransportRetryMode
	// RetryTimeouts permits retries for attempt-level timeouts while the total
	// operation context remains active.
	RetryTimeouts bool
	// RespectRetryAfter honors delta-seconds and HTTP-date Retry-After values
	// only after the response has already qualified for a retry. Setting it to
	// false disables server-directed retry timing and requires MaxRetryAfter to
	// be zero.
	RespectRetryAfter bool
	// MaxRetryAfter bounds the server-requested delay. Retry-After cannot
	// shorten the configured policy delay or extend the total operation context.
	// It must be positive when RespectRetryAfter is true.
	MaxRetryAfter time.Duration
}

RetryConfig defines a complete retry policy. Containers define its zero-value behavior: Config.Retry selects DefaultRetryConfig, while CheckConfig.Retry performs one attempt with no automatic retries. Any non-zero value is a complete replacement rather than a merge with defaults. MaxAttempts includes the initial request. Requests with non-replayable bodies are never retried. TransportErrors and RetryTimeouts independently control transport failures. RetrySafety is an independent semantic gate: configured methods retry only when the operation is intrinsically idempotent or explicitly asserted idempotent. RetrySafety also governs method-preserving redirects independently of this configuration.

func DefaultRetryConfig

func DefaultRetryConfig() RetryConfig

DefaultRetryConfig returns the production retry policy. Default retries are limited to GET, HEAD, OPTIONS, PUT, and DELETE. Transient-looking and unknown transport failures may retry, while recognized TLS failures, DNS not-found, and invalid no-response/no-error transport results fail immediately. Retry-After delta-seconds and HTTP-date values are honored only for otherwise retryable responses, bounded by DefaultMaxRetryAfter, and cannot shorten the policy delay or extend the total operation context. Custom configurations are complete replacements; callers changing selected defaults should modify this returned value.

func NoRetryConfig

func NoRetryConfig() RetryConfig

NoRetryConfig disables retries by allowing one total attempt. It retains valid bounded Retry-After defaults, although they cannot be consulted.

type RetrySafety

type RetrySafety string

RetrySafety defines whether Clientkit may automatically repeat one logical HTTP operation with equivalent method semantics. It applies both to Clientkit-scheduled retry attempts and to following method-preserving 307 and 308 redirects. RetryConfig.Methods remains an independent gate for scheduled retries, and request bodies must still be mechanically replayable. Clientkit neither generates nor inspects idempotency keys, and Retry-After cannot authorize an otherwise unsafe retry. Repeating a request after a timeout or redirect may duplicate side effects unless the remote operation is genuinely idempotent or application-level deduplication is in place. RetrySafety does not control retries internal to a RoundTripper, intermediaries, or the remote system and cannot guarantee exactly-once delivery.

const (
	// RetrySafetyDefault uses built-in HTTP method semantics. GET, HEAD,
	// OPTIONS, TRACE, PUT, and DELETE may pass the retry and 307/308 redirect
	// safety gate; POST, PATCH, CONNECT, and custom methods do not.
	RetrySafetyDefault RetrySafety = ""
	// RetrySafetyNever disables automatic retries and rejects 307/308 redirects
	// for one operation while still allowing its initial Clientkit execution
	// attempt. It does not disable ordinary 301, 302, or 303 redirect handling.
	RetrySafetyNever RetrySafety = "never"
	// RetrySafetyIdempotent asserts that repeating the complete operation through
	// a retry or 307/308 redirect is semantically safe. This is an application
	// assertion, not a Clientkit guarantee, and body replayability and all other
	// applicable policy gates still apply.
	RetrySafetyIdempotent RetrySafety = "idempotent"
)

type TransportRetryMode

type TransportRetryMode string

TransportRetryMode controls whether non-timeout HTTP transport failures may be retried. Every mode remains subject to the configured method, attempt limit, RetrySafety, request-body replayability, and operation context. RetryTimeouts independently controls recognized timeouts.

const (
	// TransportRetryNone disables retries for non-timeout transport failures.
	// It is the zero value so an explicit RetryConfig does not opt in silently.
	TransportRetryNone TransportRetryMode = ""
	// TransportRetryDefault retries transient-looking and unclassified transport
	// failures, but not recognized TLS failures, DNS not-found responses, or an
	// invalid no-response/no-error result from a RoundTripper.
	TransportRetryDefault TransportRetryMode = "default"
	// TransportRetryAll retries every non-timeout transport execution failure.
	// It is the escape hatch for callers that intentionally want broader behavior.
	TransportRetryAll TransportRetryMode = "all"
)

Directories

Path Synopsis
Package otel provides Clientkit's OpenTelemetry HTTP propagation and per-RoundTrip transport instrumentation.
Package otel provides Clientkit's OpenTelemetry HTTP propagation and per-RoundTrip transport instrumentation.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL