Documentation
¶
Overview ¶
Package relay provides a production-grade, declarative HTTP client for Go.
relay is designed with the ergonomics of Python's requests, Kotlin's OkHttp, and Java's OpenFeign - batteries included.
Quick start:
client := relay.New(
relay.WithBaseURL("https://api.example.com"),
relay.WithTimeout(10 * time.Second),
)
resp, err := client.Execute(client.Get("/users/42"))
See https://github.com/jhonsferg/relay for full documentation.
API Stability ¶
relay follows Semantic Versioning. The following rules apply to the public API defined in this package and all ext/* modules:
- All exported types, functions, and methods listed under "Stable API" below will not have backward-incompatible changes before v1.0.
- Types and functions listed under "Experimental API" may change between minor releases. They are guarded by godoc comments that say "experimental".
- The internal/ package is not part of the public API and may change at any time.
## Stable API (guaranteed no breaking changes before v1.0)
Core client:
- New, Client, Config, Option
- Client.Execute, Client.Get, Client.Post, Client.Put, Client.Patch, Client.Delete, Client.Head, Client.Options
- Client.ExecuteAsync, Client.ExecuteBatch, Client.ExecuteStream
- Client.With, Client.Shutdown, Client.BaseURL
- Request builder — all chaining methods (.Header, .Query, .Body, .Path, …)
- Response — Status, StatusCode, Body, Headers, Timing
Error handling:
- HTTPError, IsHTTPError, ErrorClass, ClassifyError
- IsTransientError, IsPermanentError, IsRateLimitedError, IsRetryableError, IsTimeout, IsCircuitOpen
- ErrCertificatePinMismatch, ErrRetryBudgetExhausted
Resilience options:
- WithRetry, RetryConfig, WithDisableRetry
- WithCircuitBreaker, CircuitBreakerConfig, CircuitBreaker, CircuitBreakerState, WithDisableCircuitBreaker
- WithRateLimit, RateLimitConfig
- WithLoadBalancer, LoadBalancerConfig, LoadBalancerStrategy
- WithTimeout, WithAdaptiveTimeout, AdaptiveTimeoutConfig
- [WithBulkhead], WithHedging, WithHedgingN
- WithRetryBudget, RetryBudget, ErrRetryBudgetExhausted
Auth:
- [WithBasicAuth], BasicAuthCreds
- [WithBearerToken]
- WithDigestAuth
- WithRequestSigner, RequestSigner, RequestSignerFunc
- WithSigner, HMACRequestSigner, MultiSigner, NewMultiSigner
Transport:
- WithBaseURL, WithConnectionPool, WithTimeout
- WithTLSConfig, WithCertificatePinning, WithClientCert, WithClientCertPEM, WithDynamicTLSCert, WithCertWatcher
- WithProxy, WithTransportMiddleware
- WithUnixSocket
- WithCompression, WithDisableCompression, CompressionAlgorithm
- [WithHTTP2], WithHTTP2PushHandler, PushPromiseHandler
Caching:
Streaming & async:
- ExecuteAs, ExecuteAsStream, DecodeAs, DecodeJSON, DecodeXML
- AsyncResult, [ExecuteAsync], [ExecuteAsyncCallback]
- BatchResult, [ExecuteBatch]
- SSEEvent, SSEHandler, [ExecuteSSE], SSEFanOut, NewSSEFanOut
- StreamResponse
Observability:
- WithHARRecording, WithHARRecorder, HARRecorder, NewHARRecorder
- HAR, HAREntry, HARLog, HARRequest, HARResponse
- WithLogger, Logger, SlogAdapter, NewDefaultLogger, NoopLogger
- RequestTiming
Hooks:
- WithOnBeforeRequest, WithOnAfterResponse, WithOnErrorHook
- OnErrorHookFunc, BeforeRetryHookFunc, BeforeRedirectHookFunc
URL building:
- WithAutoNormaliseURL, WithURLNormalisation, URLNormalisationMode
- NormaliseBaseURL, NewPathBuilder, PathBuilder
## Experimental API (may change in minor releases)
The following are functional and tested but their exact signatures or option shapes may be refined before v1.0:
- [WithSchemaValidation], JSONSchemaValidator, StructValidator, NewJSONSchemaValidator, NewStructValidator, SchemaValidator, ValidationError — schema validation API is stabilising
- SRVResolver, NewSRVResolver, SRVBalancer, SRVOption, ResolutionResult — SRV discovery API may gain a Watch() method
- CredentialProvider, RotatingTokenProvider, NewRotatingTokenProvider, WithCredentialProvider, StaticCredentialProvider — credential rotation API shape is under review
- NewPushedResponseCache, PushedResponseCache — HTTP/2 push cache API
- WSConn — WebSocket API wrapping is experimental; consider ext/websocket
- LongPollResult — long polling API may merge with SSE in a future release
WASM / js target ¶
The core relay package compiles and runs under GOOS=js GOARCH=wasm using Go's Fetch API backend. Features that rely on OS-level networking (e.g. WithUnixSocket) are silently ignored on that target. Extension modules under ext/ may have additional native dependencies and are not guaranteed to be WASM-compatible.
Index ¶
- Variables
- func DecodeAs[T any](resp *Response) (T, error)
- func DecodeJSON[T any](resp *Response) (T, error)
- func DecodeXML[T any](resp *Response) (T, error)
- func ExecuteAsStream[T any](c *Client, req *Request, handler func(T) bool) error
- func IsCircuitOpen(err error) bool
- func IsPermanentError(err error, resp *Response) bool
- func IsRateLimitedError(err error, resp *Response) bool
- func IsRetryableError(err error, resp *Response) bool
- func IsTimeout(err error) bool
- func IsTransientError(err error, resp *Response) bool
- func NormaliseBaseURL(urlStr string) string
- func PutResponse(r *Response)
- type AdaptiveTimeoutConfig
- type AsyncResult
- type BasicAuthCreds
- type BatchResult
- type BeforeRedirectHookFunc
- type BeforeRetryHookFunc
- type CacheStore
- type CachedResponse
- type CertWatcher
- type CircuitBreaker
- type CircuitBreakerConfig
- type CircuitBreakerState
- type Client
- func (c *Client) BaseURL() string
- func (c *Client) CircuitBreakerState() CircuitBreakerState
- func (c *Client) CloseIdleConnections()
- func (c *Client) Delete(url string) *Request
- func (c *Client) Execute(req *Request) (resp *Response, err error)
- func (c *Client) ExecuteAsync(req *Request) <-chan AsyncResult
- func (c *Client) ExecuteAsyncCallback(req *Request, onSuccess func(*Response), onError func(error))
- func (c *Client) ExecuteBatch(ctx context.Context, requests []*Request, maxConcurrency int) []BatchResult
- func (c *Client) ExecuteJSON(req *Request, out interface{}) (*Response, error)
- func (c *Client) ExecuteLongPoll(ctx context.Context, req *Request, prevETag string, timeout time.Duration) (LongPollResult, error)
- func (c *Client) ExecuteSSE(req *Request, handler SSEHandler) error
- func (c *Client) ExecuteSSEStream(ctx context.Context, req *Request) (<-chan SSEEvent, <-chan error)
- func (c *Client) ExecuteSSEWithReconnect(req *Request, cfg SSEClientConfig, handler SSEHandler) error
- func (c *Client) ExecuteStream(req *Request) (*StreamResponse, error)
- func (c *Client) ExecuteWebSocket(ctx context.Context, req *Request) (*WSConn, error)
- func (c *Client) Get(url string) *Request
- func (c *Client) Head(url string) *Request
- func (c *Client) IsHealthy() bool
- func (c *Client) Options(url string) *Request
- func (c *Client) Paginate(ctx context.Context, req *Request, fn PageFunc) error
- func (c *Client) PaginateWith(ctx context.Context, req *Request, nextFn NextPageFunc, fn PageFunc) error
- func (c *Client) Patch(url string) *Request
- func (c *Client) Post(url string) *Request
- func (c *Client) Put(url string) *Request
- func (c *Client) ResetCircuitBreaker()
- func (c *Client) Shutdown(ctx context.Context) error
- func (c *Client) With(opts ...Option) *Client
- type CompressionAlgorithm
- type Config
- type CredentialProvider
- type Credentials
- type DNSCacheConfig
- type DeduplicationConfig
- type ErrorClass
- type HAR
- type HARContent
- type HARCreator
- type HAREntry
- type HARLog
- type HARNameVal
- type HARPostData
- type HARRecorder
- func (r *HARRecorder) All() iter.Seq[HAREntry]
- func (r *HARRecorder) Entries() []HAREntry
- func (r *HARRecorder) EntryCount() int
- func (r *HARRecorder) Export() ([]byte, error)
- func (r *HARRecorder) ExportHAR() *HAR
- func (r *HARRecorder) ExportJSON() ([]byte, error)
- func (r *HARRecorder) Middleware() func(http.RoundTripper) http.RoundTripper
- func (r *HARRecorder) Reset()
- type HARRequest
- type HARResponse
- type HARTimings
- type HMACRequestSigner
- type HTTPError
- type HealthCheckConfig
- type JSONSchemaValidator
- type LoadBalancerConfig
- type LoadBalancerStrategy
- type Logger
- type LongPollResult
- type MultiSigner
- type MultipartField
- type NextPageFunc
- type OnErrorHookFunc
- type Option
- func WithAdaptiveTimeout(cfg AdaptiveTimeoutConfig) Option
- func WithAutoIdempotencyKey() Option
- func WithAutoIdempotencyOnSafeRetries() Option
- func WithAutoNormaliseURL(enable bool) Option
- func WithBaseURL(urlStr string) Option
- func WithBeforeRedirectHook(fn BeforeRedirectHookFunc) Option
- func WithBeforeRetryHook(fn BeforeRetryHookFunc) Option
- func WithCache(store CacheStore) Option
- func WithCertWatcher(w *CertWatcher) Option
- func WithCertificatePinning(pins []string) Option
- func WithCircuitBreaker(cbc *CircuitBreakerConfig) Option
- func WithClientCert(certFile, keyFile string) Option
- func WithClientCertPEM(certPEM, keyPEM []byte) Option
- func WithCompression(algo CompressionAlgorithm) Option
- func WithConnectionPool(maxIdle, maxIdlePerHost, maxPerHost int) Option
- func WithCookieJar(jar http.CookieJar) Option
- func WithCredentialProvider(p CredentialProvider) Option
- func WithCustomDialer(dialer *net.Dialer) Option
- func WithDNSCache(ttl time.Duration) Option
- func WithDNSOverride(hosts map[string]string) Option
- func WithDefaultAccept(accept string) Option
- func WithDefaultCookieJar() Option
- func WithDefaultHeaders(headers map[string]string) Option
- func WithDialKeepAlive(d time.Duration) Option
- func WithDialTimeout(d time.Duration) Option
- func WithDigestAuth(username, password string) Option
- func WithDisableCircuitBreaker() Option
- func WithDisableCompression() Option
- func WithDisableRetry() Option
- func WithDisableTiming() Option
- func WithDynamicTLSCert(certFile, keyFile string, interval time.Duration) Option
- func WithErrorDecoder(fn func(statusCode int, body []byte) error) Option
- func WithExpectContinueTimeout(d time.Duration) Option
- func WithHARRecorder(rec *HARRecorder) Option
- func WithHARRecording(rec *HARRecorder) Option
- func WithHTTP2PushHandler(handler PushPromiseHandler) Option
- func WithHealthCheck(url string, interval, timeout time.Duration, expectedStatus int) Option
- func WithHedging(after time.Duration) Option
- func WithHedgingN(after time.Duration, maxAttempts int) Option
- func WithIdleConnTimeout(d time.Duration) Option
- func WithInMemoryCache(maxEntries int) Option
- func WithLoadBalancer(cfg LoadBalancerConfig) Option
- func WithLogger(l Logger) Option
- func WithMaxConcurrentRequests(n int) Option
- func WithMaxRedirects(n int) Option
- func WithMaxResponseBodyBytes(n int64) Option
- func WithOnAfterResponse(hook func(context.Context, *Response) error) Option
- func WithOnBeforeRequest(hook func(context.Context, *Request) error) Option
- func WithOnErrorHook(fn OnErrorHookFunc) Option
- func WithOnRetry(fn func(attempt int, resp *http.Response, err error)) Option
- func WithOnStateChange(fn func(from, to CircuitBreakerState)) Option
- func WithPriorityQueue() Option
- func WithProxy(proxyURL string) Option
- func WithRateLimit(rps float64, burst int) Option
- func WithRequestCoalescing() Option
- func WithRequestCompression(algo CompressionAlgorithm, minBytes int) Option
- func WithRequestDeduplication() Option
- func WithRequestLogger(logger Logger) Option
- func WithRequestSigner(s RequestSigner) Option
- func WithResponseDecoder(fn func(contentType string, body []byte, v any) error) Option
- func WithResponseHeaderTimeout(d time.Duration) Option
- func WithResponseValidator(v SchemaValidator) Option
- func WithRetry(rc *RetryConfig) Option
- func WithRetryBudget(b *RetryBudget) Option
- func WithRetryIf(fn func(resp *http.Response, err error) bool) Option
- func WithRootCA(caPEM []byte) Option
- func WithSRVDiscovery(resolver *SRVResolver) Option
- func WithSigner(s RequestSigner) Option
- func WithTLSConfig(tlsCfg *tls.Config) Option
- func WithTLSHandshakeTimeout(d time.Duration) Option
- func WithTimeout(d time.Duration) Option
- func WithTransportAdapter(scheme string, rt http.RoundTripper) Option
- func WithTransportMiddleware(mw ...func(http.RoundTripper) http.RoundTripper) Option
- func WithURLNormalisation(mode URLNormalisationMode) Option
- func WithUnixSocket(socketPath string) Option
- func WithWebSocketDialTimeout(d time.Duration) Option
- type PageFunc
- type PathBuilder
- type Priority
- type ProgressFunc
- type PushPromiseHandler
- type PushedResponseCache
- type RateLimitConfig
- type RedirectInfo
- type Request
- func (r *Request) Clone() *Request
- func (r *Request) Method() string
- func (r *Request) Priority() Priority
- func (r *Request) Tag(key string) string
- func (r *Request) Tags() map[string]string
- func (r *Request) URL() string
- func (r *Request) WithAPIKey(headerName, apiKey string) *Request
- func (r *Request) WithAccept(accept string) *Request
- func (r *Request) WithBasicAuth(username, password string) *Request
- func (r *Request) WithBearerToken(token string) *Request
- func (r *Request) WithBody(body []byte) *Request
- func (r *Request) WithBodyReader(reader io.Reader) *Request
- func (r *Request) WithContentType(ct string) *Request
- func (r *Request) WithContext(ctx context.Context) *Request
- func (r *Request) WithDeduplication(enabled ...bool) *Request
- func (r *Request) WithDownloadProgress(fn ProgressFunc) *Request
- func (r *Request) WithFormData(data map[string]string) *Request
- func (r *Request) WithHeader(key, value string) *Request
- func (r *Request) WithHeaders(headers map[string]string) *Request
- func (r *Request) WithIdempotencyKey(key string) *Request
- func (r *Request) WithJSON(v interface{}) *Request
- func (r *Request) WithMaxBodySize(n int64) *Request
- func (r *Request) WithMultipart(fields []MultipartField) *Request
- func (r *Request) WithPathParam(key, value string) *Request
- func (r *Request) WithPathParams(params map[string]string) *Request
- func (r *Request) WithPriority(p Priority) *Request
- func (r *Request) WithQueryParam(key, value string) *Request
- func (r *Request) WithQueryParamValues(key string, values []string) *Request
- func (r *Request) WithQueryParams(params map[string]string) *Request
- func (r *Request) WithRequestID(id string) *Request
- func (r *Request) WithTag(key, value string) *Request
- func (r *Request) WithTimeout(d time.Duration) *Request
- func (r *Request) WithUploadProgress(fn ProgressFunc) *Request
- func (r *Request) WithUserAgent(ua string) *Request
- type RequestSigner
- type RequestSignerFunc
- type RequestTiming
- type ResolutionResult
- type Response
- func (r *Response) AsHTTPError() *HTTPError
- func (r *Response) Body() []byte
- func (r *Response) BodyReader() io.Reader
- func (r *Response) Bytes() []byte
- func (r *Response) ContentType() string
- func (r *Response) Cookies() []*http.Cookie
- func (r *Response) Decode(v any) error
- func (r *Response) Header(key string) string
- func (r *Response) IsClientError() bool
- func (r *Response) IsError() bool
- func (r *Response) IsRedirect() bool
- func (r *Response) IsServerError() bool
- func (r *Response) IsSuccess() bool
- func (r *Response) IsTruncated() bool
- func (r *Response) JSON(v interface{}) error
- func (r *Response) Location() string
- func (r *Response) Raw() *http.Response
- func (r *Response) RedirectChain() []RedirectInfo
- func (r *Response) String() string
- func (r *Response) Text() string
- func (r *Response) WasRedirected() bool
- func (r *Response) XML(v interface{}) error
- type RetryBudget
- type RetryConfig
- type RotatingTokenProvider
- type SRVBalancer
- type SRVOption
- type SRVResolver
- type SSEClientConfig
- type SSEEvent
- type SSEFanOut
- type SSEHandler
- type SchemaValidator
- type StreamResponse
- type StructValidator
- type URLNormalisationMode
- type ValidationError
- type WSConn
Constants ¶
This section is empty.
Variables ¶
var ( // ErrCircuitOpen is returned when the circuit breaker is in the Open state // and the request is rejected without being sent. ErrCircuitOpen = errors.New("circuit breaker is open") // ErrMaxRetriesReached is returned when all retry attempts have been // exhausted and the last attempt ended with a retryable error. ErrMaxRetriesReached = errors.New("max retries reached") // ErrRateLimitExceeded is returned when the rate limiter cannot grant a // token before the request context expires. ErrRateLimitExceeded = errors.New("rate limit exceeded") // ErrNilRequest is returned when a nil *Request is passed to Execute. ErrNilRequest = errors.New("request cannot be nil") // ErrTimeout wraps context.DeadlineExceeded when the per-request timeout // set via WithTimeout fires. ErrTimeout = errors.New("request timed out") // ErrBodyTruncated is a sentinel that callers may check against when they // need to detect truncation programmatically; the actual truncation is // signalled by Response.IsTruncated(). ErrBodyTruncated = errors.New("response body exceeded size limit and was truncated") // ErrClientClosed is returned when Execute is called after Shutdown. ErrClientClosed = errors.New("client is closed") // ErrBulkheadFull is returned when the bulkhead limit is reached and the // context is cancelled before a slot becomes available. ErrBulkheadFull = errors.New("relay: bulkhead full") )
Sentinel errors returned by Execute, ExecuteStream, and related methods.
var ErrCertificatePinMismatch = errors.New("relay: TLS certificate pin mismatch")
ErrCertificatePinMismatch is returned when the server's certificate chain does not contain any certificate whose SHA-256 fingerprint matches the configured pins.
var ErrRetryBudgetExhausted = errors.New("relay: retry budget exhausted")
ErrRetryBudgetExhausted is returned when the retry budget is exceeded.
Functions ¶
func DecodeAs ¶ added in v0.1.16
DecodeAs decodes the response body into a value of type T using the response's content-type-aware decoder. When a WithResponseDecoder has been configured on the client, it is used; otherwise the method falls back to JSON for application/json content and XML for application/xml.
Use DecodeAs when you fetch the response manually and decode it separately, or when you want content-type-driven dispatch without specifying the format.
order, err := relay.DecodeAs[Order](resp)
func DecodeJSON ¶ added in v0.1.16
DecodeJSON decodes the response body as JSON into a value of type T without requiring a pre-allocated target. It is the typed equivalent of calling Response.JSON on a freshly allocated pointer.
user, err := relay.DecodeJSON[User](resp)
func DecodeXML ¶ added in v0.1.16
DecodeXML decodes the response body as XML into a value of type T.
envelope, err := relay.DecodeXML[SOAPEnvelope](resp)
func ExecuteAsStream ¶ added in v0.1.1
ExecuteAsStream sends req and decodes the response body as newline-delimited JSON (JSONL / NDJSON), invoking handler for each decoded value of type T.
The stream is consumed lazily - each line is decoded on demand rather than buffering the entire response. handler is called synchronously in the goroutine that called ExecuteAsStream.
Return false from handler to stop reading and close the connection early. ExecuteAsStream returns when handler returns false, the stream ends, or an I/O or decoding error occurs.
Like Client.ExecuteStream, no retry logic is applied and the connection is closed automatically when ExecuteAsStream returns.
Example:
type LogLine struct { Level string; Message string }
err := relay.ExecuteAsStream[LogLine](client, client.Get("/logs"), func(l LogLine) bool {
fmt.Println(l.Level, l.Message)
return true
})
func IsCircuitOpen ¶ added in v0.1.16
IsCircuitOpen reports whether the error was caused by the circuit breaker being in the Open state (i.e. ErrCircuitOpen).
func IsPermanentError ¶
IsPermanentError reports whether the error will not succeed on retry (e.g. 400 Bad Request, 401 Unauthorised, 403 Forbidden, 404 Not Found).
func IsRateLimitedError ¶
IsRateLimitedError reports whether the error is a 429 Too Many Requests.
func IsRetryableError ¶ added in v0.1.16
IsRetryableError reports whether the error is worth retrying - that is, whether ClassifyError returns ErrorClassTransient or ErrorClassRateLimited. It is a convenience shorthand for the common "should I back off and try again?" decision.
func IsTimeout ¶ added in v0.1.16
IsTimeout reports whether the error represents a request timeout. It matches both ErrTimeout (returned when a per-request timeout fires) and the standard context.DeadlineExceeded sentinel.
func IsTransientError ¶
IsTransientError reports whether the error may succeed on a subsequent call.
func NormaliseBaseURL ¶ added in v0.1.4
NormaliseBaseURL ensures a base URL ends with a trailing slash if it has a non-empty, non-root path. For host-only URLs (e.g., "http://api.com"), the slash is only added if the URL doesn't already end with one. This prevents the common mistake of losing path components during URL resolution.
Examples:
- "http://api.com" → "http://api.com/"
- "http://api.com/v1" → "http://api.com/v1/"
- "http://api.com/v1/" → "http://api.com/v1/"
- "" → ""
func PutResponse ¶ added in v0.1.2
func PutResponse(r *Response)
PutResponse returns a Response to the pool. The Response must not be used after calling this function. Callers should call this when they are done with the response and not retaining it.
Types ¶
type AdaptiveTimeoutConfig ¶ added in v0.1.22
type AdaptiveTimeoutConfig struct {
// Percentile is the latency percentile to use as the base (e.g. 0.95 for p95).
// Default: 0.95.
Percentile float64
// Multiplier scales the percentile latency to get the timeout (e.g. 2.0 = 2x p95).
// Default: 2.0.
Multiplier float64
// WindowSize is the number of recent observations to keep.
// Default: 100.
WindowSize int
// MinTimeout is the minimum timeout regardless of observed latency.
// Default: 100ms.
MinTimeout time.Duration
// MaxTimeout is the maximum timeout cap.
// Default: 30s.
MaxTimeout time.Duration
// InitialTimeout is used until enough observations accumulate.
// Default: 5s.
InitialTimeout time.Duration
}
AdaptiveTimeoutConfig defines the parameters for adaptive timeout adjustment based on observed response latencies.
type AsyncResult ¶
AsyncResult holds the outcome of an asynchronous HTTP request.
type BasicAuthCreds ¶ added in v0.1.33
BasicAuthCreds holds username/password for HTTP basic auth.
type BatchResult ¶
type BatchResult struct {
// Index is the position of this result in the original request slice,
// allowing callers to correlate results with their requests.
Index int
// Response is the HTTP response, or nil if Err is non-nil.
Response *Response
// Err is the error returned by Execute, or nil on success.
Err error
}
BatchResult holds the outcome of a single request within a batch.
type BeforeRedirectHookFunc ¶ added in v0.1.16
BeforeRedirectHookFunc is called before each redirect is followed. Returning a non-nil error stops the redirect chain and propagates as the Client.Execute return value.
type BeforeRetryHookFunc ¶ added in v0.1.16
type BeforeRetryHookFunc func(ctx context.Context, attempt int, req *Request, httpResp *http.Response, err error)
BeforeRetryHookFunc is called before each retry sleep. attempt is 1-based (first retry = 1). req is the original relay Request. httpResp and err reflect the result that triggered the retry; either may be nil.
type CacheStore ¶
type CacheStore interface {
// Get returns the cached entry for key and true if found, or nil and false
// if the key is absent or the entry has expired.
Get(key string) (*CachedResponse, bool)
// Set stores or replaces the entry for key.
Set(key string, entry *CachedResponse)
// Delete removes the entry for key. It is a no-op if the key is absent.
Delete(key string)
// Clear removes all entries from the store.
Clear()
}
CacheStore is the storage backend for the HTTP response cache. Implement this interface to plug in Redis, Memcached, disk, or any other store. All methods must be safe for concurrent use.
func NewInMemoryCacheStore ¶
func NewInMemoryCacheStore(maxEntries int) CacheStore
NewInMemoryCacheStore returns an in-memory CacheStore with the given capacity. When at capacity, expired entries are evicted first; then the oldest by insertion order. Passing maxEntries <= 0 defaults to 256.
type CachedResponse ¶
type CachedResponse struct {
// StatusCode is the HTTP status code of the original response.
StatusCode int
// Status is the human-readable status line (e.g. "200 OK").
Status string
// Headers is a clone of the original response headers.
Headers http.Header
// Body is the full response body bytes.
Body []byte
// ExpiresAt is the absolute time after which this entry must not be served.
// A zero value means the entry has no expiry derived from the response.
ExpiresAt time.Time
// ETag is the value of the ETag response header, used for conditional
// revalidation via If-None-Match on subsequent requests.
ETag string
// LastModified is the value of the Last-Modified response header, used for
// conditional revalidation via If-Modified-Since when ETag is absent.
LastModified string
}
CachedResponse holds a serialised HTTP response for replay. It is stored by [cachingTransport] after a cache-eligible response is received and returned on subsequent matching requests until the entry expires.
type CertWatcher ¶ added in v0.1.19
type CertWatcher struct {
// contains filtered or unexported fields
}
CertWatcher hot-reloads a TLS certificate/key pair from disk at a fixed interval without restarting the process. Use WithDynamicTLSCert to create one and attach it to a Client, or WithCertWatcher to manage the lifecycle yourself.
func (*CertWatcher) Stop ¶ added in v0.1.19
func (w *CertWatcher) Stop()
Stop halts the background reload goroutine. It is safe to call more than once and from multiple goroutines concurrently; only the first call has any effect.
type CircuitBreaker ¶
type CircuitBreaker struct {
// contains filtered or unexported fields
}
CircuitBreaker implements the three-state circuit-breaker pattern (Closed → Open → Half-Open → Closed). It is safe for concurrent use.
The fast path - Allow() when the breaker is in StateClosed - uses an atomic load and returns without acquiring the mutex, minimising lock contention on healthy services. All state machine transitions and counter mutations are still serialised under mu.
func (*CircuitBreaker) Allow ¶
func (cb *CircuitBreaker) Allow() bool
Allow reports whether a request should be attempted given the current state. In StateOpen it transitions to StateHalfOpen when the reset timeout has elapsed. In StateHalfOpen it limits concurrent probes to HalfOpenRequests.
Fast path: when the breaker is Closed, the state is read atomically with no mutex acquisition, avoiding contention on healthy services.
func (*CircuitBreaker) RecordFailure ¶
func (cb *CircuitBreaker) RecordFailure()
RecordFailure records a failed response or transport error. In StateClosed it increments the failure counter and trips to StateOpen when MaxFailures is reached. In StateHalfOpen a single failure immediately re-opens the breaker.
func (*CircuitBreaker) RecordSuccess ¶
func (cb *CircuitBreaker) RecordSuccess()
RecordSuccess records a successful response. In StateClosed it resets the failure counter. In StateHalfOpen it increments the success counter and transitions to StateClosed once SuccessThreshold is reached.
func (*CircuitBreaker) Reset ¶
func (cb *CircuitBreaker) Reset()
Reset forces the breaker back to StateClosed and clears all counters. Useful after a manual health check confirms that the downstream has recovered.
func (*CircuitBreaker) State ¶
func (cb *CircuitBreaker) State() CircuitBreakerState
State returns the current CircuitBreakerState without modifying any counters. Uses an atomic load - no mutex acquisition required.
type CircuitBreakerConfig ¶
type CircuitBreakerConfig struct {
// MaxFailures is the number of consecutive failures in StateClosed that
// trips the breaker to StateOpen. Must be > 0.
MaxFailures int
// ResetTimeout is how long the breaker stays in StateOpen before
// automatically transitioning to StateHalfOpen to probe recovery.
ResetTimeout time.Duration
// HalfOpenRequests is the maximum number of probe requests allowed while
// in StateHalfOpen. Additional requests are rejected until the breaker
// decides to close or re-open.
HalfOpenRequests int
// SuccessThreshold is the number of consecutive successes required while
// in StateHalfOpen to transition back to StateClosed.
SuccessThreshold int
// OnStateChange is an optional callback invoked on every state transition.
// It receives the previous and new states. The callback is invoked OUTSIDE
// the breaker's internal mutex - it is safe to call breaker methods from
// within it.
OnStateChange func(from, to CircuitBreakerState)
}
CircuitBreakerConfig controls the thresholds and behaviour of the breaker.
type CircuitBreakerState ¶
type CircuitBreakerState int
CircuitBreakerState represents the current state of the circuit breaker.
const ( // StateClosed is the normal operating state. All requests pass through and // failures are counted. When failures reach MaxFailures the breaker trips // to StateOpen. StateClosed CircuitBreakerState = iota // StateHalfOpen is the recovery probe state. A limited number of requests // are allowed through to test whether the downstream has recovered. On // enough consecutive successes the breaker transitions to StateClosed; on // any failure it returns to StateOpen. StateHalfOpen // StateOpen is the tripped state. All requests are rejected immediately // with [ErrCircuitOpen] without reaching the network. After ResetTimeout // the breaker transitions to StateHalfOpen automatically. StateOpen )
func (CircuitBreakerState) String ¶
func (s CircuitBreakerState) String() string
String returns the human-readable name of the state.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is a production-grade HTTP client with a configurable transport stack, automatic retry/backoff, circuit breaker, token-bucket rate limiter, HTTP response caching, OpenTelemetry distributed tracing and metrics, streaming, batch, and async execution.
A zero-value Client is not usable; always construct via New or Client.With. The client is safe for concurrent use by multiple goroutines.
func New ¶
New creates a Client from the provided functional options. Options are applied on top of sensible defaults (30 s timeout, 3-attempt exponential backoff, 5-failure circuit breaker). The client is immutable after construction; use Client.With to derive variants with different settings.
client := httpclient.New(
httpclient.WithBaseURL("https://api.example.com"),
httpclient.WithTimeout(10*time.Second),
)
func (*Client) BaseURL ¶ added in v0.3.0
BaseURL returns the base URL configured for this client, or an empty string if no base URL was set. Useful when composing relay clients with other libraries that need to inherit the URL (e.g. traverse).
func (*Client) CircuitBreakerState ¶
func (c *Client) CircuitBreakerState() CircuitBreakerState
CircuitBreakerState returns the current state of the circuit breaker. Returns StateClosed when no circuit breaker is configured.
func (*Client) CloseIdleConnections ¶
func (c *Client) CloseIdleConnections()
CloseIdleConnections closes any idle connections currently held in the transport's connection pool without interrupting active requests.
func (*Client) Execute ¶
Execute sends the request through the full pipeline:
closed-guard -> ctx guard -> OnBeforeRequest hooks -> rate limiter -> circuit breaker -> retrier -> transport stack -> OnAfterResponse hooks
A non-nil error is returned for transport-level failures, context cancellations, and when the circuit breaker is open. HTTP error status codes (4xx, 5xx) are NOT converted to errors - inspect Response.IsError or call Response.AsHTTPError to handle them.
func (*Client) ExecuteAsync ¶
func (c *Client) ExecuteAsync(req *Request) <-chan AsyncResult
ExecuteAsync sends the request in a background goroutine and returns a buffered channel that receives exactly one AsyncResult when the request completes. The channel is closed after delivery.
Use a select with a context-aware case to impose an external deadline:
ch := client.ExecuteAsync(req)
select {
case result := <-ch:
if result.Err != nil { ... }
fmt.Println(result.Response.StatusCode)
case <-ctx.Done():
// The request continues in the background; its own context governs it.
}
func (*Client) ExecuteAsyncCallback ¶
ExecuteAsyncCallback sends the request in a background goroutine and invokes the appropriate callback on completion. Either callback may be nil.
client.ExecuteAsyncCallback(req,
func(resp *Response) { log.Println("ok", resp.StatusCode) },
func(err error) { log.Println("failed", err) },
)
func (*Client) ExecuteBatch ¶
func (c *Client) ExecuteBatch(ctx context.Context, requests []*Request, maxConcurrency int) []BatchResult
ExecuteBatch sends all requests concurrently and returns their results in the same order as the input slice. maxConcurrency caps the number of simultaneous in-flight requests; pass 0 or a negative value for fully parallel execution (len(requests) workers).
ctx governs how long to wait for a concurrency slot. Individual request timeouts and contexts are set on each Request via Request.WithTimeout or Request.WithContext - they are independent of the batch ctx.
All results are populated before ExecuteBatch returns; no result slot is ever left at its zero value.
results := client.ExecuteBatch(ctx, []*httpclient.Request{
client.Get("/users/1"),
client.Get("/users/2"),
client.Get("/users/3"),
}, 5)
for _, r := range results {
if r.Err != nil { ... }
fmt.Println(r.Index, r.Response.StatusCode)
}
func (*Client) ExecuteJSON ¶
ExecuteJSON is a convenience wrapper that calls [Execute] and, on a successful (2xx) response, unmarshals the body into out via encoding/json. If out is nil the unmarshal step is skipped. The Response is always returned alongside any unmarshal error.
var order Order
resp, err := client.ExecuteJSON(
client.Post("/orders").WithJSON(payload),
&order,
)
func (*Client) ExecuteLongPoll ¶ added in v0.1.25
func (c *Client) ExecuteLongPoll(ctx context.Context, req *Request, prevETag string, timeout time.Duration) (LongPollResult, error)
ExecuteLongPoll sends a long-polling request. It sets a long timeout, sends If-None-Match with prevETag (if non-empty), and returns LongPollResult. A 304 Not Modified is treated as success with Modified=false.
Typical usage:
result, err := client.ExecuteLongPoll(ctx, client.Get("/resource"), "", 55*time.Second)
if err != nil {
return err
}
if result.Modified {
fmt.Println("New data:", result.Response.String())
}
// Use result.ETag for the next poll
func (*Client) ExecuteSSE ¶ added in v0.1.1
func (c *Client) ExecuteSSE(req *Request, handler SSEHandler) error
ExecuteSSE sends req and reads the response body as a Server-Sent Events stream, invoking handler for each complete event. The method returns when the handler returns false, the stream ends, or an I/O error occurs.
ExecuteSSE calls Client.ExecuteStream internally and therefore inherits all of its semantics: no retry logic, participates in graceful drain, and the connection is closed automatically when ExecuteSSE returns.
Typical usage:
err := client.ExecuteSSE(
client.Get("/events").WithHeader("Accept", "text/event-stream"),
func(ev relay.SSEEvent) bool {
fmt.Println(ev.Event, ev.Data)
return true // continue
},
)
func (*Client) ExecuteSSEStream ¶ added in v0.1.25
func (c *Client) ExecuteSSEStream(ctx context.Context, req *Request) (<-chan SSEEvent, <-chan error)
ExecuteSSEStream returns a channel of SSEEvents and an error channel. Events are sent to the channel; the caller controls consumption rate. Close ctx to stop the stream.
The caller must read from both channels until one is closed. The channels are closed when the stream ends or an error occurs.
Typical usage:
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
events, errs := client.ExecuteSSEStream(ctx, client.Get("/events"))
for {
select {
case ev, ok := <-events:
if !ok {
return
}
fmt.Println(ev.Event, ev.Data)
case err, ok := <-errs:
if !ok {
return
}
fmt.Printf("error: %v\n", err)
return
}
}
func (*Client) ExecuteSSEWithReconnect ¶ added in v0.1.25
func (c *Client) ExecuteSSEWithReconnect(req *Request, cfg SSEClientConfig, handler SSEHandler) error
ExecuteSSEWithReconnect sends req and reads SSE stream with automatic reconnection. On disconnect, it re-sends the request with the Last-Event-ID header set to the last received event ID.
Typical usage:
cfg := relay.SSEClientConfig{
MaxReconnects: 5,
ReconnectDelay: 1*time.Second,
EventTypes: []string{"update", "notification"},
}
err := client.ExecuteSSEWithReconnect(
client.Get("/events"),
cfg,
func(ev relay.SSEEvent) bool {
fmt.Println(ev.Event, ev.Data)
return true
},
)
func (*Client) ExecuteStream ¶
func (c *Client) ExecuteStream(req *Request) (*StreamResponse, error)
ExecuteStream sends the request and returns a StreamResponse without buffering the body. Retry logic is NOT applied - a streaming response cannot be replayed once partially consumed.
The request participates in Client.Shutdown's graceful drain: the in-flight counter is released when Body.Close() is called, not when ExecuteStream returns.
[Config.OnBeforeRequest] hooks are applied before the request is sent. [Config.OnAfterResponse] hooks are NOT called because the body has not yet been consumed when ExecuteStream returns.
If a per-request timeout is set via Request.WithTimeout, the deadline context is canceled automatically when Body.Close() is called.
func (*Client) ExecuteWebSocket ¶ added in v0.1.19
ExecuteWebSocket upgrades a request to a WebSocket connection. req must target a "ws://" or "wss://" URL (or an "http://"/"https://" URL which is transparently rewritten). The client's TLS config, default headers, and request signer are applied to the upgrade handshake.
The caller is responsible for calling WSConn.Close when done.
func (*Client) IsHealthy ¶
IsHealthy reports whether the circuit breaker is in the Closed or Half-Open state, meaning the client will attempt to send requests. Returns true when no circuit breaker is configured.
func (*Client) Paginate ¶ added in v0.1.17
Paginate iterates through pages of results, calling fn for each page. It follows the Link header (rel="next") by default. Use PaginateWith for custom next-page extraction.
err := client.Paginate(ctx, client.Get("/items"), func(resp *relay.Response) (bool, error) {
var items []Item
if err := resp.JSON(&items); err != nil {
return false, err
}
process(items)
return true, nil
})
func (*Client) PaginateWith ¶ added in v0.1.17
func (c *Client) PaginateWith(ctx context.Context, req *Request, nextFn NextPageFunc, fn PageFunc) error
PaginateWith iterates through pages of results using a custom next-page extractor. nextFn is called after each page to get the URL for the next page; an empty string signals the end.
func (*Client) ResetCircuitBreaker ¶
func (c *Client) ResetCircuitBreaker()
ResetCircuitBreaker forces the circuit breaker back to the Closed state and clears all failure counters. Useful after a manual health check confirms downstream recovery.
func (*Client) Shutdown ¶
Shutdown gracefully stops the client. It marks the client as closed (new [Execute] calls immediately return ErrClientClosed), cancels all background goroutines (health check, etc.), waits for all in-flight requests - including open streaming bodies - to finish, then closes idle connections in the pool.
If ctx expires before the drain completes, Shutdown returns ctx.Err() but does NOT forcefully abort in-flight requests - their own contexts govern that.
type CompressionAlgorithm ¶ added in v0.1.25
type CompressionAlgorithm int
CompressionAlgorithm specifies the compression algorithm used for Accept-Encoding negotiation and response/request body compression.
const ( // CompressionAuto advertises all supported encodings (zstd, br, gzip, deflate) // and decompresses responses automatically. This is the recommended default. CompressionAuto CompressionAlgorithm = iota // CompressionZstd selects Zstandard (zstd) compression only. CompressionZstd // CompressionBrotli selects Brotli (br) compression only. CompressionBrotli // CompressionGzip selects gzip compression only. CompressionGzip )
type Config ¶
type Config struct {
// BaseURL is prepended to every request path that does not start with
// "http://" or "https://". Trailing slashes are normalised automatically.
BaseURL string
// Timeout is the end-to-end deadline for a complete request/response cycle,
// including all retry attempts. Set per-request timeouts via
// [Request.WithTimeout] for finer-grained control.
Timeout time.Duration
// MaxIdleConns is the maximum total number of idle (keep-alive) connections
// across all hosts in the pool.
MaxIdleConns int
// MaxIdleConnsPerHost is the maximum number of idle connections kept per
// individual host.
MaxIdleConnsPerHost int
// MaxConnsPerHost limits the total number of connections (active + idle) per
// host. 0 means unlimited.
MaxConnsPerHost int
// IdleConnTimeout is how long an idle keep-alive connection remains open
// before being evicted from the pool.
IdleConnTimeout time.Duration
// TLSHandshakeTimeout is the deadline for completing the TLS handshake.
TLSHandshakeTimeout time.Duration
// ResponseHeaderTimeout is the deadline to read the response headers after
// the request body has been sent. 0 disables the timeout.
ResponseHeaderTimeout time.Duration
// DialTimeout is the maximum time allowed for a TCP connection to be
// established.
DialTimeout time.Duration
// DialKeepAlive is the interval between TCP keep-alive probes sent on an
// active connection.
DialKeepAlive time.Duration
// ExpectContinueTimeout is the maximum time to wait for a server's first
// response headers after fully writing the request headers if the request
// has an Expect: 100-continue header. Zero means no specific timeout.
ExpectContinueTimeout time.Duration
// TLSConfig replaces the default TLS configuration. When nil, a config
// enforcing TLS 1.2 as the minimum version is used.
TLSConfig *tls.Config
// ProxyURL is the URL of the HTTP/HTTPS proxy to use for all requests.
// When empty, the proxy is sourced from the HTTP_PROXY / HTTPS_PROXY
// environment variables.
ProxyURL string
// CookieJar manages cookie storage across requests. Set to nil to disable
// automatic cookie handling.
CookieJar http.CookieJar
// CacheStore is the backend used to cache HTTP responses. When nil, caching
// is disabled. Use [NewInMemoryCacheStore] or implement [CacheStore] for
// custom backends such as Redis.
CacheStore CacheStore
// CustomDialer replaces the default net.Dialer. When set, DialTimeout and
// DialKeepAlive are ignored in favour of the dialer's own settings.
CustomDialer *net.Dialer
// DNSOverrides maps hostnames to fixed IP addresses, bypassing DNS
// resolution for those hosts. Example: {"api.internal": "10.0.0.42"}.
DNSOverrides map[string]string
// RetryConfig controls retry behaviour. When nil, a sensible default
// (3 attempts, exponential backoff) is used.
RetryConfig *RetryConfig
// RetryBudget limits the fraction of requests that may be retried within a
// sliding window to prevent retry storms. When nil, no budget is enforced.
RetryBudget *RetryBudget
// CircuitBreakerConfig controls the circuit breaker. When nil, a default
// (5 failures → Open, 60 s reset) is used. Set explicitly to nil with
// [WithDisableCircuitBreaker] to disable it entirely.
CircuitBreakerConfig *CircuitBreakerConfig
// RateLimitConfig enables the client-side token-bucket rate limiter.
// When nil, rate limiting is disabled.
RateLimitConfig *RateLimitConfig
// LoadBalancerConfig distributes requests across multiple backend URLs.
// When set, BaseURL is ignored and a backend is selected per request.
// When nil, load balancing is disabled.
LoadBalancerConfig *LoadBalancerConfig
// DefaultHeaders are merged into every outgoing request. Per-request headers
// always take precedence over these defaults.
DefaultHeaders map[string]string
// DisableCompression disables automatic Accept-Encoding negotiation and
// transparent response decompression.
DisableCompression bool
// RequestCompression enables transparent gzip compression of request bodies.
// Requests with no body are not affected. Content-Encoding is set automatically.
// Set via [WithRequestCompression] or [WithRequestCompressionLevel].
RequestCompression bool
// RequestCompressionLevel is the gzip compression level (0-9).
// A value of 0 means gzip.DefaultCompression is used.
// Only meaningful when RequestCompression is true.
RequestCompressionLevel int
// DisableTiming skips per-request timing instrumentation (httptrace).
// When true, [Response.Timing] fields are all zero and roughly 10
// allocations per request are avoided. Useful for high-throughput
// scenarios where timing metrics are not needed.
DisableTiming bool
// MaxRedirects is the maximum number of redirects to follow automatically.
// Set to 0 to disable redirect following.
MaxRedirects int
// MaxResponseBodyBytes is the maximum number of body bytes buffered by
// [Execute]. Responses exceeding this limit are truncated and
// [Response.IsTruncated] returns true. Set to 0 for no limit (default 10 MB).
MaxResponseBodyBytes int64
// TransportMiddlewares is a chain of [http.RoundTripper] wrappers applied
// around the core transport. Middleware is applied outermost-last - the last
// appended middleware is the first to intercept a request.
TransportMiddlewares []func(http.RoundTripper) http.RoundTripper
// OnBeforeRequest is a list of hooks called before each request attempt
// (including retries). A hook that returns a non-nil error cancels the
// request immediately.
OnBeforeRequest []func(context.Context, *Request) error
// OnAfterResponse is a list of hooks called after a successful response is
// received (after all retries, before returning to the caller). A hook that
// returns a non-nil error propagates as the [Client.Execute] return value.
OnAfterResponse []func(context.Context, *Response) error
// ErrorDecoder is called by [Client.Execute] whenever the HTTP response
// status code is >= 400. It receives the numeric status code and the full,
// already-buffered response body. When the function returns a non-nil error,
// [Client.Execute] releases the response and returns that error directly to
// the caller. When it returns nil the response is returned normally,
// preserving the default behaviour where HTTP error codes are not
// automatically treated as errors.
//
// ErrorDecoder is invoked after all [OnAfterResponse] hooks have run.
// Use [WithErrorDecoder] to set it.
ErrorDecoder func(statusCode int, body []byte) error
// ResponseDecoder is used by [Response.Decode] and [ExecuteAs] to
// deserialise response bodies into Go values. When set, it replaces the
// default encoding/json and encoding/xml decoders. The contentType
// parameter receives the response Content-Type header (e.g.
// "application/json", "application/protobuf") so the decoder can pick
// the right format.
//
// Returning a non-nil error from ResponseDecoder propagates as the error
// from [Response.Decode] or [ExecuteAs].
//
// Use [WithResponseDecoder] to set it.
ResponseDecoder func(contentType string, body []byte, v any) error
// ResponseValidator is applied after each successful (2xx) response body
// is read. The body is decoded into an interface{} and passed to Validate.
// If validation fails, Execute returns a [ValidationError].
//
// Use [WithResponseValidator] to set it.
ResponseValidator SchemaValidator
// Signer is called for each outgoing HTTP request, after all headers and
// the idempotency key have been applied, and before the request is sent.
// Use it to implement request authentication that must inspect and/or
// mutate the final *http.Request (e.g. HMAC-SHA256, OAuth 1.0a, JWS).
//
// Returning a non-nil error from Sign aborts the request attempt and
// propagates as the [Client.Execute] error.
//
// Use [WithSigner] to set it.
Signer RequestSigner
// CredentialProvider is called before each outgoing request (including
// retries) to supply fresh credentials. It runs before [Signer], allowing
// dynamic tokens (e.g. OAuth, Vault) without rebuilding the client.
//
// Use [WithCredentialProvider] to set it.
CredentialProvider CredentialProvider
// Logger is used for internal structured logging (retries, circuit-breaker
// transitions, rate-limit events, shutdown). Defaults to NoopLogger.
Logger Logger
// TLSPins is a list of SHA-256 certificate fingerprints in the format
// "sha256/BASE64==". When non-empty, TLS connections whose certificate chain
// does not match any pin are rejected with ErrCertificatePinMismatch.
TLSPins []string
// EnableCoalescing activates request deduplication for concurrent identical
// GET/HEAD requests. Only one real request is made; all waiters share a copy.
EnableCoalescing bool
// Deduplication configures opt-in singleflight request deduplication.
// When Enabled, concurrent GET/HEAD requests with the same URL are
// collapsed into a single real HTTP call. All callers receive their own
// copy of the response body. Disabled by default.
Deduplication DeduplicationConfig
// DigestUsername and DigestPassword enable HTTP Digest Authentication.
DigestUsername string
DigestPassword string
// HARRecorder captures all request/response pairs when non-nil.
HARRecorder *HARRecorder
// HTTP2PushHandler is called for each HTTP/2 push-promised response.
// See [WithHTTP2PushHandler] for details and current limitations.
HTTP2PushHandler PushPromiseHandler
// AutoIdempotencyKey automatically injects an X-Idempotency-Key header on
// every request. The same key is reused across retry attempts.
AutoIdempotencyKey bool
// AutoIdempotencyOnSafeRetries is like AutoIdempotencyKey but restricts
// key injection to HTTP methods that are semantically idempotent or safe:
// GET, HEAD, PUT, OPTIONS, and TRACE. POST, PATCH, and DELETE are skipped
// unless the caller sets the key explicitly. The same key is reused across
// all retry attempts for the request.
AutoIdempotencyOnSafeRetries bool
// BeforeRetryHooks are called before each retry sleep, in order.
// attempt is 1-based. httpResp and err reflect the result that triggered
// the retry; either may be nil. Use [WithBeforeRetryHook] to append hooks.
BeforeRetryHooks []BeforeRetryHookFunc
// BeforeRedirectHooks are called before each redirect is followed.
// Returning a non-nil error stops the redirect chain; the error propagates
// as the [Client.Execute] return value. Use [WithBeforeRedirectHook].
BeforeRedirectHooks []BeforeRedirectHookFunc
// OnErrorHooks are called when [Client.Execute] returns a non-nil error.
// They run after all internal error handling and are intended for logging
// and metrics; the return value is discarded. Use [WithOnErrorHook].
OnErrorHooks []OnErrorHookFunc
// HealthCheck enables a background goroutine that proactively probes a
// health endpoint while the circuit breaker is open and resets it on
// success. Nil disables the feature.
HealthCheck *HealthCheckConfig
// DNSCache enables client-side DNS caching with a configurable TTL.
// Nil disables the feature (default: OS resolver on every dial).
DNSCache *DNSCacheConfig
// URLNormalisationMode controls how [BaseURL] is resolved against request
// paths. Default is NormalisationAuto, which intelligently detects API URLs
// and applies the best strategy. Can be overridden with [WithURLNormalisation].
URLNormalisationMode URLNormalisationMode
// AutoNormaliseBaseURL automatically adds a trailing slash to BaseURL if
// missing. Enabled by default for API convenience; can be disabled with
// [WithAutoNormaliseURL](false).
AutoNormaliseBaseURL bool
// MaxConcurrentRequests is the maximum number of in-flight requests
// allowed simultaneously. Zero or negative means no limit.
MaxConcurrentRequests int
// EnablePriorityQueue activates priority-aware dequeuing when the bulkhead
// is at capacity. When enabled, higher-priority requests are dequeued before
// lower-priority ones. Requests within the same priority level maintain FIFO
// order. Disabled by default (all requests use PriorityNormal).
// Only has an effect when MaxConcurrentRequests > 0.
EnablePriorityQueue bool
// DefaultAccept is the value sent in the Accept header when no explicit
// Accept header has been set on the request. Empty string means no default
// Accept header is added.
DefaultAccept string
// UnixSocketPath is the filesystem path of a Unix domain socket to use for
// all outgoing connections. When set, the TCP dialler is replaced with a
// Unix socket dialler so that the client communicates with a local server
// (e.g. the Docker daemon at /var/run/docker.sock) instead of opening a
// TCP connection. The host in the request URL is still sent as the HTTP
// Host header; only the transport layer is changed.
//
// HTTP/2 is disabled for Unix socket connections because it is not
// typically negotiated over local sockets.
//
// Set via [WithUnixSocket].
UnixSocketPath string
// HedgeAfter is the delay before sending a duplicate (hedge) request.
// Zero disables hedging. When set, a second request is sent after this
// duration if the first has not completed. The first response wins.
HedgeAfter time.Duration
// HedgeMaxAttempts is the maximum number of concurrent hedge requests
// (including the original). Defaults to 2 when HedgeAfter > 0.
HedgeMaxAttempts int
// CertWatcher holds the active dynamic TLS certificate watcher created by
// [WithDynamicTLSCert] or [WithCertWatcher]. Callers can call Stop() to
// halt the background reload goroutine.
CertWatcher *CertWatcher
// WebSocketDialTimeout is the handshake timeout for [Client.ExecuteWebSocket].
// Zero uses the client Timeout.
WebSocketDialTimeout time.Duration
// SchemeAdapters maps URL schemes to custom [http.RoundTripper] instances.
// Requests whose scheme matches a key are dispatched to the corresponding
// transport instead of the default HTTP/HTTPS transport. Set via
// [WithTransportAdapter].
SchemeAdapters map[string]http.RoundTripper
// AdaptiveTimeoutConfig enables adaptive timeout adjustment based on
// observed response latencies. When set, per-request timeouts are
// dynamically computed from the percentile of recent latencies.
// When nil, adaptive timeout is disabled.
AdaptiveTimeoutConfig *AdaptiveTimeoutConfig
// contains filtered or unexported fields
}
Config holds all configuration for a Client. It is built by applying a sequence of functional Option values on top of the defaults returned by [defaultConfig]. Do not modify a Config after passing it to [buildClient].
type CredentialProvider ¶ added in v0.1.33
type CredentialProvider interface {
Credentials(ctx context.Context) (Credentials, error)
}
CredentialProvider supplies fresh credentials before each request. Implementations may fetch tokens from a vault, refresh OAuth tokens, etc.
func StaticCredentialProvider ¶ added in v0.1.33
func StaticCredentialProvider(creds Credentials) CredentialProvider
StaticCredentialProvider always returns the same credentials.
type Credentials ¶ added in v0.1.33
type Credentials struct {
BearerToken string // sets Authorization: Bearer <token>
BasicAuth *BasicAuthCreds // sets HTTP basic auth
Headers map[string]string // arbitrary headers (e.g. X-API-Key)
}
Credentials holds values to apply to an outgoing request.
type DNSCacheConfig ¶ added in v0.1.1
type DNSCacheConfig struct {
// TTL is how long a resolved address set is considered valid.
// After expiry the next dial for that hostname triggers a fresh resolution.
// A typical value is 30 s–5 min depending on how often your upstreams
// rotate IPs and how quickly you need to detect changes.
TTL time.Duration
}
DNSCacheConfig controls client-side DNS result caching.
type DeduplicationConfig ¶ added in v0.1.22
type DeduplicationConfig struct {
// Enabled activates singleflight deduplication for safe methods (GET, HEAD).
// Default: false.
Enabled bool
}
DeduplicationConfig controls request deduplication behaviour.
type ErrorClass ¶
type ErrorClass int
ErrorClass categorises an error returned by Execute into actionable groups, allowing callers to make branching decisions without inspecting raw error types.
const ( ErrorClassNone ErrorClass = iota // no error (success or non-error status) ErrorClassTransient // may succeed on a subsequent attempt ErrorClassPermanent // will not succeed on retry (4xx) ErrorClassRateLimited // 429 Too Many Requests ErrorClassCanceled // caller canceled the context )
func ClassifyError ¶
func ClassifyError(err error, resp *Response) ErrorClass
ClassifyError returns the ErrorClass for an error (and optional response) returned by Execute. It is the primary entry point for error categorisation.
class := httpclient.ClassifyError(err, resp)
switch class {
case httpclient.ErrorClassTransient: // back off and retry upstream
case httpclient.ErrorClassRateLimited: // respect Retry-After
case httpclient.ErrorClassPermanent: // return 400 / log and skip
case httpclient.ErrorClassCanceled: // propagate context cancellation
}
func (ErrorClass) String ¶
func (c ErrorClass) String() string
String returns the human-readable class name.
type HAR ¶ added in v0.1.35
type HAR struct {
Log HARLog `json:"log"`
}
HAR is the top-level HAR 1.2 document.
type HARContent ¶
type HARContent struct {
Size int `json:"size"`
MimeType string `json:"mimeType"`
Text string `json:"text,omitempty"`
}
HARContent holds response body information.
type HARCreator ¶ added in v0.1.35
HARCreator identifies the tool that created the HAR archive.
type HAREntry ¶
type HAREntry struct {
StartedDateTime string `json:"startedDateTime"`
Time float64 `json:"time"`
Request HARRequest `json:"request"`
Response HARResponse `json:"response"`
Timings HARTimings `json:"timings"`
}
HAREntry represents a single request/response pair in HAR 1.2 format.
type HARLog ¶ added in v0.1.35
type HARLog struct {
Version string `json:"version"`
Creator HARCreator `json:"creator"`
Entries []HAREntry `json:"entries"`
}
HARLog is the top-level HAR 1.2 log object.
type HARNameVal ¶
HARNameVal is a key/value pair used for headers and query params.
type HARPostData ¶
HARPostData holds request body information.
type HARRecorder ¶
type HARRecorder struct {
// contains filtered or unexported fields
}
HARRecorder captures HTTP request/response pairs in HAR 1.2 format. It is safe for concurrent use.
func NewHARRecorder ¶
func NewHARRecorder(args ...string) *HARRecorder
NewHARRecorder creates a new, empty HARRecorder. Optionally accepts creatorName and creatorVersion as the first two variadic string arguments; both default to "relay" / "0.1.0" when omitted.
func (*HARRecorder) All ¶ added in v0.1.38
func (r *HARRecorder) All() iter.Seq[HAREntry]
All returns an iterator over a snapshot of the recorded entries. Callers may use it with range:
for entry := range recorder.All() { ... }
func (*HARRecorder) Entries ¶
func (r *HARRecorder) Entries() []HAREntry
Entries returns a snapshot of all recorded entries.
func (*HARRecorder) EntryCount ¶ added in v0.1.35
func (r *HARRecorder) EntryCount() int
EntryCount returns the number of recorded entries.
func (*HARRecorder) Export ¶
func (r *HARRecorder) Export() ([]byte, error)
Export serialises all recorded entries as a HAR 1.2 JSON document.
func (*HARRecorder) ExportHAR ¶ added in v0.1.35
func (r *HARRecorder) ExportHAR() *HAR
ExportHAR returns the recorded transactions as a HAR 1.2 document struct. Thread-safe: can be called while recording is ongoing.
func (*HARRecorder) ExportJSON ¶ added in v0.1.35
func (r *HARRecorder) ExportJSON() ([]byte, error)
ExportJSON returns the HAR 1.2 archive as pretty-printed JSON bytes. Thread-safe: can be called while recording is ongoing.
func (*HARRecorder) Middleware ¶ added in v0.1.35
func (r *HARRecorder) Middleware() func(http.RoundTripper) http.RoundTripper
Middleware returns a relay-compatible transport middleware (func(http.RoundTripper) http.RoundTripper) that records each request/response pair as a HAR entry.
type HARRequest ¶
type HARRequest struct {
Method string `json:"method"`
URL string `json:"url"`
HTTPVersion string `json:"httpVersion"`
Headers []HARNameVal `json:"headers"`
QueryString []HARNameVal `json:"queryString"`
PostData *HARPostData `json:"postData,omitempty"`
BodySize int `json:"bodySize"`
HeadersSize int `json:"headersSize"`
}
HARRequest is the HAR 1.2 request object.
type HARResponse ¶
type HARResponse struct {
Status int `json:"status"`
StatusText string `json:"statusText"`
HTTPVersion string `json:"httpVersion"`
Headers []HARNameVal `json:"headers"`
Content HARContent `json:"content"`
RedirectURL string `json:"redirectURL"`
BodySize int `json:"bodySize"`
HeadersSize int `json:"headersSize"`
}
HARResponse is the HAR 1.2 response object.
type HARTimings ¶
type HARTimings struct {
Send float64 `json:"send"`
Wait float64 `json:"wait"`
Receive float64 `json:"receive"`
}
HARTimings holds HAR 1.2 timing breakdown.
type HMACRequestSigner ¶ added in v0.1.30
type HMACRequestSigner struct {
// Key is the HMAC-SHA256 signing key. Must not be empty.
Key []byte
// Header is the name of the signature header.
// Defaults to "X-Signature" when empty.
Header string
}
HMACRequestSigner signs requests with HMAC-SHA256 over a canonical string composed of the request method, URL, and a UTC timestamp. It sets two headers:
- X-Timestamp (RFC3339 UTC) - replay-protection timestamp.
- X-Signature (hex-encoded HMAC-SHA256) - computed over "METHOD\nURL\nTIMESTAMP".
The signing key must be kept secret. HMACRequestSigner is safe for concurrent use.
type HTTPError ¶
HTTPError represents an HTTP response whose status code indicates a client or server error (4xx or 5xx). It is returned by Response.AsHTTPError and can be used with errors.As.
func IsHTTPError ¶
IsHTTPError reports whether err (or any error in its chain) is an *HTTPError and returns it if so. Use this instead of errors.As when you want both the bool and the typed value in one call.
type HealthCheckConfig ¶ added in v0.1.1
type HealthCheckConfig struct {
// URL is the HTTP endpoint to probe. It must be a fully-qualified URL
// (e.g. "https://api.example.com/health"). The client issues a plain GET
// using the standard library - not through the relay pipeline - so the
// probe is not subject to the main circuit breaker, retries, or rate limiter.
URL string
// Interval is how often to send a probe while the circuit is Open.
// Smaller values recover faster at the cost of more background traffic.
// A reasonable default is 10–30 seconds.
Interval time.Duration
// Timeout is the per-probe HTTP deadline. It should be shorter than
// Interval so a slow probe does not delay the next one.
Timeout time.Duration
// ExpectedStatus is the HTTP status code that signals a healthy upstream.
// Any 2xx status is accepted when ExpectedStatus is 0.
ExpectedStatus int
}
HealthCheckConfig controls the background health-probe goroutine that proactively tests whether a tripped circuit breaker should be reset.
When the circuit breaker is Open, the relay client normally waits for [CircuitBreakerConfig.ResetTimeout] to elapse before it allows a probe request through. WithHealthCheck shortens this recovery time by actively polling a dedicated health endpoint in the background and resetting the breaker as soon as the endpoint responds with a healthy status.
type JSONSchemaValidator ¶ added in v0.1.35
type JSONSchemaValidator struct {
// contains filtered or unexported fields
}
JSONSchemaValidator validates a response against a minimal JSON Schema subset. Supported keywords: type, required, properties, minLength, maxLength, minimum, maximum, pattern.
func NewJSONSchemaValidator ¶ added in v0.1.35
func NewJSONSchemaValidator(schemaJSON string) (*JSONSchemaValidator, error)
NewJSONSchemaValidator creates a JSONSchemaValidator from a JSON Schema string.
func (*JSONSchemaValidator) Validate ¶ added in v0.1.35
func (j *JSONSchemaValidator) Validate(v interface{}) error
Validate validates v against the JSON Schema.
type LoadBalancerConfig ¶ added in v0.1.22
type LoadBalancerConfig struct {
// Backends is the list of base URLs to balance across.
// Must not be empty when used.
Backends []string
// Strategy selects the load balancing algorithm.
// Defaults to RoundRobin if empty.
Strategy LoadBalancerStrategy
}
LoadBalancerConfig configures client-side load balancing across multiple backends.
type LoadBalancerStrategy ¶ added in v0.1.22
type LoadBalancerStrategy string
LoadBalancerStrategy defines the algorithm used to select backends.
const ( // RoundRobin distributes requests sequentially across backends. // This is the default strategy. RoundRobin LoadBalancerStrategy = "round-robin" // Random selects a backend uniformly at random on each request. Random LoadBalancerStrategy = "random" )
type Logger ¶
type Logger interface {
Debug(msg string, args ...any)
Info(msg string, args ...any)
Warn(msg string, args ...any)
Error(msg string, args ...any)
}
Logger is the interface used by relay for internal structured logging. Compatible with log/slog.Logger and most popular logging libraries.
func NewDefaultLogger ¶
NewDefaultLogger creates a new slog-backed Logger that writes to stderr at the given minimum level. Suitable for quick setups and tests.
func NoopLogger ¶
func NoopLogger() Logger
NoopLogger returns a Logger that silently discards all messages. This is the default when no logger is configured.
func SlogAdapter ¶
SlogAdapter wraps an existing *slog.Logger so it can be passed to WithLogger. Use this when your application already has a configured slog instance.
type LongPollResult ¶ added in v0.1.25
type LongPollResult struct {
// Modified is true when the server returned a new response (not 304).
Modified bool
// Response is the full response when Modified is true. Nil on 304.
Response *Response
// ETag is the ETag from the response (for the next poll).
ETag string
}
LongPollResult is returned by ExecuteLongPoll.
type MultiSigner ¶ added in v0.1.30
type MultiSigner struct {
// contains filtered or unexported fields
}
MultiSigner chains multiple RequestSigner implementations, applying each in order. The first error encountered stops the chain and is returned. MultiSigner itself implements RequestSigner and is safe for concurrent use if all constituent signers are safe for concurrent use.
func NewMultiSigner ¶ added in v0.1.30
func NewMultiSigner(signers ...RequestSigner) *MultiSigner
NewMultiSigner creates a MultiSigner that applies each signer in the order provided. Nil entries are silently skipped.
type MultipartField ¶
type MultipartField struct {
// FieldName is the form field name (the name attribute in HTML).
FieldName string
// FileName is the filename reported to the server. A non-empty value
// creates a file part; an empty value creates a plain form field.
FileName string
// ContentType is an optional MIME type for file parts.
// When empty, multipart.Writer uses application/octet-stream.
ContentType string
// Content holds in-memory file or field data. Use Reader instead for
// streaming sources such as os.File to avoid loading the whole file.
Content []byte
// Reader is a streaming data source. Takes precedence over Content when
// both are non-nil. The caller is responsible for closing it after Execute.
Reader io.Reader
}
MultipartField represents a single part in a multipart/form-data request. Set FileName to create a file part; leave it empty for a plain form field. Reader takes precedence over Content when both are set. ContentType overrides the default application/octet-stream for file parts.
type NextPageFunc ¶ added in v0.1.17
NextPageFunc extracts the URL for the next page from a response. Return an empty string to signal the last page.
type OnErrorHookFunc ¶ added in v0.1.16
OnErrorHookFunc is called when Client.Execute returns a non-nil error. It is intended for logging and metrics; its return value is discarded.
type Option ¶
type Option func(*Config)
Option is a functional option that mutates a Config during New or Client.With. Options are applied left-to-right; later options win.
func WithAdaptiveTimeout ¶ added in v0.1.22
func WithAdaptiveTimeout(cfg AdaptiveTimeoutConfig) Option
WithAdaptiveTimeout enables adaptive timeout adjustment based on observed response latencies. The client tracks recent response times and computes per-request timeouts as a percentile of that data, multiplied by a factor. When disabled (nil), all requests use the fixed client timeout.
func WithAutoIdempotencyKey ¶
func WithAutoIdempotencyKey() Option
WithAutoIdempotencyKey automatically injects an X-Idempotency-Key header (UUID v4) into every request. The same key is reused across retry attempts, preventing duplicate side effects on servers that support idempotency keys.
func WithAutoIdempotencyOnSafeRetries ¶ added in v0.1.16
func WithAutoIdempotencyOnSafeRetries() Option
WithAutoIdempotencyOnSafeRetries automatically injects an X-Idempotency-Key header for HTTP methods that are semantically idempotent or safe (GET, HEAD, PUT, OPTIONS, TRACE). POST, PATCH, and DELETE are skipped unless the caller sets a key explicitly. The same key is reused across all retry attempts for a given request.
Use WithAutoIdempotencyKey instead if you want to inject the key for all methods unconditionally.
func WithAutoNormaliseURL ¶ added in v0.1.4
WithAutoNormaliseURL enables or disables automatic trailing slash normalisation for base URLs. Enabled by default. When disabled, WithBaseURL passes the URL as-is without modification.
func WithBaseURL ¶
WithBaseURL sets the base URL prepended to every request path that does not start with "http://" or "https://". The URL is pre-parsed once for performance. If [Config.AutoNormaliseBaseURL] is true (the default), a trailing slash is automatically added if missing.
func WithBeforeRedirectHook ¶ added in v0.1.16
func WithBeforeRedirectHook(fn BeforeRedirectHookFunc) Option
WithBeforeRedirectHook appends a hook invoked before each redirect. Returning a non-nil error aborts the redirect chain.
WithBeforeRedirectHook(func(req *http.Request, via []*http.Request) error {
if len(via) > 3 { return errors.New("too many redirects") }
return nil
})
func WithBeforeRetryHook ¶ added in v0.1.16
func WithBeforeRetryHook(fn BeforeRetryHookFunc) Option
WithBeforeRetryHook appends a hook invoked before each retry sleep. Multiple hooks are called in the order they are registered.
WithBeforeRetryHook(func(ctx context.Context, attempt int, req *relay.Request, httpResp *http.Response, err error) {
slog.InfoContext(ctx, "retrying", "attempt", attempt, "err", err)
})
func WithCache ¶
func WithCache(store CacheStore) Option
WithCache attaches a custom CacheStore to the client. Use WithInMemoryCache for the built-in option, or implement CacheStore for Redis, disk, or other backends.
func WithCertWatcher ¶ added in v0.1.19
func WithCertWatcher(w *CertWatcher) Option
WithCertWatcher attaches a pre-constructed CertWatcher to the client. The watcher's GetClientCertificate hook is installed on the TLS config. Callers are responsible for starting and stopping the watcher's lifecycle.
func WithCertificatePinning ¶
WithCertificatePinning rejects TLS connections whose certificate chain does not contain any certificate matching the provided SHA-256 pins. Pins must be base64-encoded SHA-256 digests, optionally prefixed with "sha256/".
func WithCircuitBreaker ¶
func WithCircuitBreaker(cbc *CircuitBreakerConfig) Option
WithCircuitBreaker replaces the circuit breaker configuration.
func WithClientCert ¶ added in v0.1.26
WithClientCert configures the client to present a static TLS client certificate for mutual TLS (mTLS) authentication. The certificate and key are loaded once at construction time; use WithDynamicTLSCert instead when hot-reloading is required.
Example:
client := relay.New(
relay.WithClientCert("/certs/client.crt", "/certs/client.key"),
)
func WithClientCertPEM ¶ added in v0.1.26
WithClientCertPEM configures the client to present a TLS client certificate loaded from PEM-encoded bytes. Use this when certificates are sourced from environment variables, secret managers (Vault, AWS Secrets Manager), or in-memory configuration rather than disk files.
Example:
client := relay.New(
relay.WithClientCertPEM(certPEM, keyPEM),
)
func WithCompression ¶ added in v0.1.25
func WithCompression(algo CompressionAlgorithm) Option
WithCompression returns an Option that enables transparent response decompression. The client advertises the chosen algorithm(s) via Accept-Encoding and automatically decompresses responses whose Content-Encoding matches.
When CompressionAuto is used the header sent is:
Accept-Encoding: zstd, br, gzip, deflate
Existing WithDisableCompression or transport-level compression settings are unaffected; this middleware operates at the transport layer.
func WithConnectionPool ¶
WithConnectionPool tunes the connection pool size.
- maxIdle: maximum total idle (keep-alive) connections across all hosts.
- maxIdlePerHost: maximum idle connections per host.
- maxPerHost: maximum total connections per host (0 = unlimited).
func WithCookieJar ¶
WithCookieJar sets the cookie jar used by the client. Pass nil to disable automatic cookie handling.
func WithCredentialProvider ¶ added in v0.1.33
func WithCredentialProvider(p CredentialProvider) Option
WithCredentialProvider sets a CredentialProvider that is called before each request attempt (including retries). It is applied after default headers and the idempotency key, at the same point as WithSigner.
When both a CredentialProvider and a RequestSigner are configured, the provider runs first, then the signer.
func WithCustomDialer ¶
WithCustomDialer replaces the default net.Dialer. When set, the dial timeout and keep-alive configured via WithDialTimeout / WithDialKeepAlive are ignored in favour of the dialer's own settings.
func WithDNSCache ¶ added in v0.1.1
WithDNSCache enables client-side DNS caching so that each unique hostname is resolved at most once per ttl interval. This reduces DNS lookup latency on keep-alive-heavy workloads and avoids thundering-herd re-resolution when many goroutines dial the same host simultaneously.
The cache is per-client; different New calls each maintain their own cache. Entries are evicted lazily (on next access) when their TTL expires.
func WithDNSOverride ¶
WithDNSOverride maps hostnames to specific IP addresses, bypassing DNS resolution for those hosts. Useful for service discovery, split-horizon DNS, and integration testing without modifying /etc/hosts.
WithDNSOverride(map[string]string{"api.internal": "10.0.0.42"})
func WithDefaultAccept ¶ added in v0.1.17
WithDefaultAccept sets the default Accept header sent when the request does not already carry one. Empty string disables the default.
func WithDefaultCookieJar ¶
func WithDefaultCookieJar() Option
WithDefaultCookieJar creates and attaches a standard RFC 6265 cookie jar. The jar persists cookies across requests for the lifetime of the client.
func WithDefaultHeaders ¶
WithDefaultHeaders merges the given headers into every outgoing request. Per-request headers always take precedence over these defaults. Header values are sanitised to strip CR/LF characters.
func WithDialKeepAlive ¶
WithDialKeepAlive sets the interval between TCP keep-alive probes on active connections.
func WithDialTimeout ¶
WithDialTimeout sets the maximum time allowed for the TCP dial to complete.
func WithDigestAuth ¶
WithDigestAuth enables HTTP Digest Authentication (RFC 7616). The client automatically handles the 401 challenge/response cycle.
func WithDisableCircuitBreaker ¶
func WithDisableCircuitBreaker() Option
WithDisableCircuitBreaker removes the circuit breaker entirely so all requests are attempted regardless of upstream failure rates.
func WithDisableCompression ¶
func WithDisableCompression() Option
WithDisableCompression disables automatic Accept-Encoding negotiation and transparent response decompression by the transport.
func WithDisableRetry ¶
func WithDisableRetry() Option
WithDisableRetry disables all retry behaviour so only a single attempt is made.
func WithDisableTiming ¶ added in v0.1.8
func WithDisableTiming() Option
WithDisableTiming skips per-request timing instrumentation. When set, [Response.Timing] fields are all zero and approximately 10 allocations per Client.Execute call are avoided. Recommended for high-throughput scenarios where timing metrics are not required.
func WithDynamicTLSCert ¶ added in v0.1.19
WithDynamicTLSCert enables hot-reloading of the TLS client certificate. The certificate and key are loaded immediately; an error is silently ignored at construction time - callers that need hard failure should use WithCertWatcher instead, where they control initialisation.
The interval controls how often the files are re-read. The returned CertWatcher is stored in Config so callers can stop it via [Config.CertWatcher].Stop() when done.
func WithErrorDecoder ¶ added in v0.1.10
WithErrorDecoder sets a function that translates HTTP error status codes (>= 400) into typed Go errors. The function receives the numeric status code and the fully-buffered response body. When it returns a non-nil error, Client.Execute releases the response and returns that error to the caller. When it returns nil the response is returned unchanged, preserving the default behaviour where HTTP error codes are not automatically errors.
The decoder runs after all WithOnAfterResponse hooks.
Example - map 404 to a sentinel:
var ErrNotFound = errors.New("not found")
client := relay.New(
relay.WithErrorDecoder(func(status int, body []byte) error {
if status == http.StatusNotFound {
return ErrNotFound
}
return nil
}),
)
func WithExpectContinueTimeout ¶ added in v0.1.17
WithExpectContinueTimeout sets the maximum time to wait for a server's first response headers when the request has an Expect: 100-continue header. Zero disables the timeout.
func WithHARRecorder ¶ added in v0.1.35
func WithHARRecorder(rec *HARRecorder) Option
WithHARRecorder is an alias for WithHARRecording.
func WithHARRecording ¶
func WithHARRecording(rec *HARRecorder) Option
WithHARRecording attaches a HARRecorder that captures every request and response in HAR 1.2 format. Call HARRecorder.Export to serialise.
func WithHTTP2PushHandler ¶ added in v0.1.39
func WithHTTP2PushHandler(handler PushPromiseHandler) Option
WithHTTP2PushHandler registers handler to be called whenever the server sends an HTTP/2 push promise.
Current limitation ¶
golang.org/x/net v0.50.0 (the version used by this module) removed the public PushHandler interface from http2.Transport and the client-side SETTINGS frame explicitly disables server push (SETTINGS_ENABLE_PUSH=0). As a result this option is a no-op at runtime: the handler is stored in the Config for forward-compatibility but is never invoked. Once the upstream transport re-exposes a push interception API the stored handler will be wired in without any change to call-sites.
If you pass a *PushedResponseCache as your handler via a closure, relay will automatically serve subsequent requests from the cache when the URL matches a pushed response.
func WithHealthCheck ¶ added in v0.1.1
WithHealthCheck enables a background health-probe goroutine. While the circuit breaker is in the Open state the client periodically issues a GET request to url with the given timeout. A response with expectedStatus (or any 2xx when expectedStatus is 0) resets the circuit breaker to Closed without waiting for the natural ResetTimeout to elapse.
The probe goroutine stops automatically when Client.Shutdown is called. WithHealthCheck has no effect when the circuit breaker is disabled.
func WithHedging ¶ added in v0.1.17
WithHedging enables request hedging: a duplicate request is sent after d if the first has not completed. The first response wins; the other is cancelled. Defaults to 2 concurrent attempts.
func WithHedgingN ¶ added in v0.1.17
WithHedgingN enables request hedging with up to maxAttempts concurrent duplicates. after is the delay between launching each attempt.
func WithIdleConnTimeout ¶
WithIdleConnTimeout sets how long an idle keep-alive connection remains open before being evicted from the pool.
func WithInMemoryCache ¶
WithInMemoryCache creates and attaches an in-memory LRU cache with the given maximum entry count.
func WithLoadBalancer ¶ added in v0.1.22
func WithLoadBalancer(cfg LoadBalancerConfig) Option
WithLoadBalancer distributes requests across multiple backend URLs using the given strategy. When set, BaseURL is ignored and each request selects a backend from cfg.Backends. Use RoundRobin (default) or Random as strategy.
func WithLogger ¶
WithLogger sets the structured logger used for internal relay events such as retries, circuit breaker state changes, and rate-limit waits. Use SlogAdapter, NewDefaultLogger, or your own implementation.
func WithMaxConcurrentRequests ¶ added in v0.1.17
WithMaxConcurrentRequests sets the maximum number of in-flight requests allowed simultaneously (bulkhead). Zero or negative means no limit.
func WithMaxRedirects ¶
WithMaxRedirects sets the maximum number of redirects to follow automatically. Set to 0 to disable redirect following entirely.
func WithMaxResponseBodyBytes ¶
WithMaxResponseBodyBytes limits how many bytes of a response body are buffered by Client.Execute. Responses that exceed this limit are silently truncated; check Response.IsTruncated. Set to 0 for no limit (default 10 MB).
func WithOnAfterResponse ¶
WithOnAfterResponse appends a hook called after each successful response is received (after all retries, before returning to the caller). Returning a non-nil error propagates as the Client.Execute return value.
func WithOnBeforeRequest ¶
WithOnBeforeRequest appends a hook called before each request attempt (including retries). Returning a non-nil error cancels the request.
func WithOnErrorHook ¶ added in v0.1.16
func WithOnErrorHook(fn OnErrorHookFunc) Option
WithOnErrorHook appends a hook invoked whenever Client.Execute returns a non-nil error. Use it for structured error logging or metrics.
WithOnErrorHook(func(ctx context.Context, req *relay.Request, err error) {
slog.ErrorContext(ctx, "request failed", "method", req.Method(), "err", err)
})
func WithOnRetry ¶
WithOnRetry registers a callback invoked before each retry sleep. Useful for structured logging and metrics. attempt is 1-based (first retry = 1).
func WithOnStateChange ¶
func WithOnStateChange(fn func(from, to CircuitBreakerState)) Option
WithOnStateChange registers a callback invoked on every circuit breaker state transition. See [CircuitBreakerConfig.OnStateChange] for constraints.
func WithPriorityQueue ¶ added in v0.1.25
func WithPriorityQueue() Option
WithPriorityQueue enables priority-aware request dequeuing when the bulkhead is at capacity. Must be used with WithMaxConcurrentRequests. When enabled, higher-priority requests are dequeued before lower-priority ones. Requests within the same priority level maintain FIFO order. Disabled by default.
func WithProxy ¶
WithProxy sets the proxy URL for all requests. Omit (or pass "") to inherit the proxy from the HTTP_PROXY / HTTPS_PROXY environment variables.
func WithRateLimit ¶
WithRateLimit enables the client-side token-bucket rate limiter.
- rps: sustained request rate in requests per second.
- burst: maximum number of tokens that can accumulate above the sustained rate.
func WithRequestCoalescing ¶
func WithRequestCoalescing() Option
WithRequestCoalescing enables deduplication of concurrent identical GET and HEAD requests. Only one real HTTP call is made; all callers sharing the same URL receive independent copies of the response body.
func WithRequestCompression ¶ added in v0.1.25
func WithRequestCompression(algo CompressionAlgorithm, minBytes int) Option
WithRequestCompression returns an Option that compresses outgoing request bodies when their serialised size exceeds minBytes (pass ≤ 0 to use the default of 1024 bytes). The Content-Encoding header is set accordingly.
CompressionAuto compresses with zstd. For Brotli or Gzip pass the corresponding constant explicitly.
Example:
client := relay.New(
relay.WithRequestCompression(relay.CompressionZstd, 512),
)
func WithRequestDeduplication ¶ added in v0.1.22
func WithRequestDeduplication() Option
WithRequestDeduplication enables singleflight-based deduplication for GET and HEAD requests. Concurrent requests to the same URL are collapsed into a single real HTTP call; all callers receive their own copy of the response. Disabled by default. Use per-request WithDeduplication to override.
func WithRequestLogger ¶ added in v0.1.26
WithRequestLogger adds a transport-level middleware that logs every request/response cycle using the provided Logger.
Requests are logged at Debug level with method and URL. Responses are logged at Debug for 2xx/3xx and at Warn for 4xx/5xx, including status code and round-trip latency in milliseconds.
Pair with SlogAdapter or NewDefaultLogger to integrate with your application's existing logging setup:
client := relay.New(
relay.WithRequestLogger(relay.SlogAdapter(slog.Default())),
)
func WithRequestSigner ¶ added in v0.1.30
func WithRequestSigner(s RequestSigner) Option
WithRequestSigner is an alias for WithSigner.
func WithResponseDecoder ¶ added in v0.1.12
WithResponseDecoder sets a custom deserialiser used by Response.Decode and ExecuteAs. When configured it replaces the built-in encoding/json and encoding/xml decoders. The contentType parameter receives the response Content-Type header value so the function can select the appropriate format.
Returning a non-nil error propagates as the error from Response.Decode or ExecuteAs.
Example - use Protocol Buffers when the server sends application/protobuf:
client := relay.New(
relay.WithResponseDecoder(func(ct string, body []byte, v any) error {
if strings.Contains(ct, "protobuf") {
return proto.Unmarshal(body, v.(proto.Message))
}
return json.Unmarshal(body, v) // fallback to JSON
}),
)
func WithResponseHeaderTimeout ¶
WithResponseHeaderTimeout sets the deadline to read response headers after the request body has been sent. 0 disables the timeout.
func WithResponseValidator ¶ added in v0.1.35
func WithResponseValidator(v SchemaValidator) Option
WithResponseValidator sets a SchemaValidator that is applied after each successful (2xx) response is decoded. If validation fails, Execute returns a ValidationError wrapping the validation failure details.
func WithRetry ¶
func WithRetry(rc *RetryConfig) Option
WithRetry replaces the entire retry configuration. Pass nil to restore the package defaults (3 attempts, exponential backoff).
func WithRetryBudget ¶ added in v0.1.21
func WithRetryBudget(b *RetryBudget) Option
WithRetryBudget sets a sliding-window retry budget that caps the fraction of requests that may be retried within the window. This prevents retry storms when a downstream service degrades. See RetryBudget for field semantics.
func WithRetryIf ¶
WithRetryIf sets a custom retry predicate on the active RetryConfig (or on the default config if none has been set yet). The predicate is evaluated when the built-in logic would retry; returning false suppresses the retry.
WithRetryIf(func(resp *http.Response, err error) bool {
if resp != nil { return resp.StatusCode == 503 }
return true
})
func WithRootCA ¶ added in v0.1.26
WithRootCA adds a PEM-encoded CA certificate to the client's TLS trust store. Use this for private PKI environments where the server certificate is signed by an internal CA not present in the system certificate pool.
Multiple calls append additional CAs without replacing earlier ones.
Example:
client := relay.New(
relay.WithRootCA(internalCAPEM),
)
func WithSRVDiscovery ¶ added in v0.1.34
func WithSRVDiscovery(resolver *SRVResolver) Option
WithSRVDiscovery sets an SRVResolver on the client. Before each request, the resolver is called and the request Host is replaced with the resolved target.
func WithSigner ¶ added in v0.1.12
func WithSigner(s RequestSigner) Option
WithSigner sets a RequestSigner that is invoked for every outgoing request. The signer receives the fully-built *http.Request and may add or modify headers, compute a body digest, or perform any operation required by the target service's authentication scheme.
WithSigner is applied once per attempt (including retries). If the body must be re-read on retries, use a body that supports io.Seeker or set the body via Request.WithBody so relay can replay it.
Example - static API key header:
client := relay.New(
relay.WithSigner(relay.RequestSignerFunc(func(r *http.Request) error {
r.Header.Set("Authorization", "Bearer "+apiKey)
return nil
})),
)
Example - HMAC-SHA256 request signing:
type HMACSigner struct{ secret []byte }
func (s *HMACSigner) Sign(r *http.Request) error {
mac := hmac.New(sha256.New, s.secret)
fmt.Fprintf(mac, "%s\n%s\n%s", r.Method, r.URL.Path, r.Header.Get("Date"))
r.Header.Set("X-Signature", base64.StdEncoding.EncodeToString(mac.Sum(nil)))
return nil
}
client := relay.New(relay.WithSigner(&HMACSigner{secret: key}))
func WithTLSConfig ¶
WithTLSConfig replaces the default TLS configuration. The default enforces TLS 1.2 as the minimum protocol version.
func WithTLSHandshakeTimeout ¶ added in v0.1.17
WithTLSHandshakeTimeout sets the deadline for completing the TLS handshake.
func WithTimeout ¶
WithTimeout sets the end-to-end request timeout (including all retry attempts). Use Request.WithTimeout for per-request control.
func WithTransportAdapter ¶ added in v0.1.19
func WithTransportAdapter(scheme string, rt http.RoundTripper) Option
WithTransportAdapter registers a custom http.RoundTripper for requests whose URL scheme matches scheme (e.g. "myproto", "grpc", "ftp"). When the scheme router encounters a request with that scheme, it dispatches to rt instead of the default transport. "http" and "https" cannot be overridden with this option; use WithTransportMiddleware for those.
func WithTransportMiddleware ¶
func WithTransportMiddleware(mw ...func(http.RoundTripper) http.RoundTripper) Option
WithTransportMiddleware appends one or more http.RoundTripper middleware functions. Middleware is applied outermost-last - the last appended middleware is the first to intercept a request.
func WithURLNormalisation ¶ added in v0.1.4
func WithURLNormalisation(mode URLNormalisationMode) Option
WithURLNormalisation sets the URL normalisation strategy for resolving base URLs against request paths. The default is NormalisationAuto, which intelligently detects API URLs and chooses the best strategy.
Modes:
func WithUnixSocket ¶ added in v0.1.29
WithUnixSocket configures the relay client to connect via a Unix domain socket. All requests will be routed through the socket at socketPath, regardless of the host in the request URL.
The baseURL still controls the HTTP host header and path; only the network transport layer is changed to use the Unix socket.
Example:
client := relay.New(
relay.WithBaseURL("http://localhost"),
relay.WithUnixSocket("/var/run/docker.sock"),
)
func WithWebSocketDialTimeout ¶ added in v0.1.19
WithWebSocketDialTimeout sets the handshake timeout used by Client.ExecuteWebSocket. Zero (the default) falls back to the client [Config.Timeout].
type PageFunc ¶ added in v0.1.17
PageFunc is called for each page of results. It receives the response for that page. Return (true, nil) to continue to the next page, (false, nil) to stop, or (false, err) to abort with an error.
type PathBuilder ¶ added in v0.1.4
type PathBuilder struct {
// contains filtered or unexported fields
}
PathBuilder provides a fluent interface for constructing relative paths by composing path segments. It uses the builder pattern to allow clean, chainable path construction.
Example:
builder := NewPathBuilder("/api/v1")
path := builder.Add("users").Add("123").Add("posts").String()
// Result: "/api/v1/users/123/posts"
func NewPathBuilder ¶ added in v0.1.4
func NewPathBuilder(base string) *PathBuilder
NewPathBuilder creates a new PathBuilder initialised with an optional base path. If base is provided, it becomes the first segment. If base is empty, the builder starts empty.
Example:
builder := NewPathBuilder("/api/v1") // Start with base path
builder := NewPathBuilder("") // Start empty
func (*PathBuilder) Add ¶ added in v0.1.4
func (pb *PathBuilder) Add(segment string) *PathBuilder
Add appends a new path segment to the builder. The segment is added as-is (slashes are handled automatically). Returns the builder for method chaining.
Example:
builder.Add("users").Add("123").Add("profile")
func (*PathBuilder) AddIfNotEmpty ¶ added in v0.1.4
func (pb *PathBuilder) AddIfNotEmpty(condition bool, segment string) *PathBuilder
AddIfNotEmpty appends segment only if the provided condition is true. Useful for conditionally building paths based on configuration.
Example:
builder.Add("users").AddIfNotEmpty(id != "", id).Add("posts")
func (*PathBuilder) Len ¶ added in v0.1.4
func (pb *PathBuilder) Len() int
Len returns the number of segments in the path (excluding the leading slash).
func (*PathBuilder) Reset ¶ added in v0.1.4
func (pb *PathBuilder) Reset() *PathBuilder
Reset clears all segments and returns the builder to an empty state. Useful for reusing a PathBuilder instance.
func (*PathBuilder) Segments ¶ added in v0.1.4
func (pb *PathBuilder) Segments() []string
Segments returns a slice of all segments in the path. This is useful for debugging or inspecting the built path.
func (*PathBuilder) String ¶ added in v0.1.4
func (pb *PathBuilder) String() string
String returns the final constructed path as a string. Segments are joined with "/" and the result always starts with "/".
Example:
builder := NewPathBuilder("api").Add("v1").Add("users")
fmt.Println(builder.String()) // Output: "/api/v1/users"
type Priority ¶ added in v0.1.25
type Priority int
Priority represents the urgency level of a request. Higher values indicate higher priority and are dequeued first when the bulkhead is at capacity. Within the same priority level, requests are dequeued in FIFO order.
const ( // PriorityLow is for background or non-critical requests. PriorityLow Priority = 0 // PriorityNormal is the default priority for typical requests. PriorityNormal Priority = 50 // PriorityHigh is for important requests that should execute sooner. PriorityHigh Priority = 100 // PriorityCritical is for time-sensitive requests (health checks, auth, etc.). PriorityCritical Priority = 200 )
type ProgressFunc ¶
type ProgressFunc func(transferred, total int64)
ProgressFunc is called periodically during upload or download with the number of bytes transferred so far and the total size. If the total is unknown (no Content-Length header), total is -1.
type PushPromiseHandler ¶ added in v0.1.39
PushPromiseHandler is called when the server sends an HTTP/2 push promise. pushedURL is the URL of the pushed resource; pushedResp is the pushed response whose body the handler is responsible for consuming and closing.
Note: active push-promise interception requires golang.org/x/net/http2 v0.x or earlier (the PushHandler interface was removed in later versions). On the current dependency (golang.org/x/net v0.50.0) the handler is registered but never invoked because the underlying transport disables server push at the HTTP/2 SETTINGS level. The type is kept in the public API so callers can prepare for future transport support without breaking changes.
type PushedResponseCache ¶ added in v0.1.39
type PushedResponseCache struct {
// contains filtered or unexported fields
}
PushedResponseCache stores push-promised responses indexed by URL. It is safe for concurrent use.
func NewPushedResponseCache ¶ added in v0.1.39
func NewPushedResponseCache() *PushedResponseCache
NewPushedResponseCache returns an initialised, empty PushedResponseCache.
func (*PushedResponseCache) Len ¶ added in v0.1.39
func (c *PushedResponseCache) Len() int
Len returns the number of entries currently held in the cache.
type RateLimitConfig ¶
type RateLimitConfig struct {
// RequestsPerSecond is the sustained token replenishment rate, i.e. the
// long-term maximum number of requests per second.
RequestsPerSecond float64
// Burst is the maximum number of tokens that can accumulate above the
// sustained rate. A burst of N allows N requests to be dispatched
// immediately before throttling begins.
Burst int
}
RateLimitConfig configures the client-side token-bucket rate limiter.
type RedirectInfo ¶ added in v0.1.16
type RedirectInfo struct {
// From is the URL of the request that received the redirect response.
From string
// To is the URL the client was redirected to.
To string
// StatusCode is the HTTP status of the redirect response (e.g. 302).
// Zero if the status could not be determined.
StatusCode int
}
RedirectInfo records a single redirect hop followed during Client.Execute.
type Request ¶
type Request struct {
// contains filtered or unexported fields
}
Request is a fluent builder for a single HTTP call. All With* methods return the receiver so they can be chained without intermediate variables.
A Request must not be shared between goroutines after it is passed to Client.Execute.
func (*Request) Clone ¶ added in v0.1.1
Clone returns a deep copy of the request. All maps and slices are independently duplicated so mutations to the clone do not affect the original, and vice-versa. The body bytes slice is also copied.
Clone is useful when the same base request needs to be dispatched with different headers, query params, or bodies without constructing from scratch.
func (*Request) Method ¶ added in v0.1.1
Method returns the HTTP verb for this request (e.g. "GET", "POST").
func (*Request) Priority ¶ added in v0.1.25
Priority returns the priority level set via Request.WithPriority. Returns PriorityNormal if not explicitly set.
func (*Request) Tag ¶
Tag returns the value of a tag previously set via Request.WithTag, or "" if the tag is absent.
func (*Request) Tags ¶
Tags returns a copy of all tags attached to this request. Returns nil if no tags have been set.
func (*Request) URL ¶ added in v0.1.1
URL returns the raw URL string as provided to the builder, before path parameters are substituted or query parameters are appended.
func (*Request) WithAPIKey ¶
WithAPIKey sets a header-based API key. The header name varies by service; common choices are "X-API-Key" and "Authorization".
req.WithAPIKey("X-API-Key", os.Getenv("SERVICE_API_KEY"))
func (*Request) WithAccept ¶
WithAccept sets the Accept request header.
func (*Request) WithBasicAuth ¶
WithBasicAuth sets the Authorization header to the RFC 7617 Basic credential for the given username and password.
func (*Request) WithBearerToken ¶
WithBearerToken sets the Authorization header to "Bearer <token>".
func (*Request) WithBody ¶
WithBody sets the raw request body bytes. The caller is responsible for also setting Content-Type via Request.WithContentType.
func (*Request) WithBodyReader ¶
WithBodyReader reads all bytes from reader and sets them as the request body. If the reader returns an error the body is left unchanged. For very large payloads prefer Client.ExecuteStream combined with a custom RoundTripper.
func (*Request) WithContentType ¶
WithContentType sets the Content-Type request header.
func (*Request) WithContext ¶
WithContext sets the context used for this request. If the context carries a deadline it races with any timeout set via Request.WithTimeout - whichever fires first cancels the request.
func (*Request) WithDeduplication ¶ added in v0.1.22
WithDeduplication overrides the client-level deduplication setting for this single request. Call with no argument or true to force deduplication on; call with false to disable it even when the client has deduplication enabled.
func (*Request) WithDownloadProgress ¶
func (r *Request) WithDownloadProgress(fn ProgressFunc) *Request
WithDownloadProgress registers a callback that is invoked periodically during response body download. transferred is bytes read so far; total is Content-Length or -1 if the header is absent.
func (*Request) WithFormData ¶
WithFormData URL-encodes data and sets Content-Type to application/x-www-form-urlencoded.
func (*Request) WithHeader ¶
WithHeader sets (or replaces) a single request header. Per-request headers take precedence over [Config.DefaultHeaders]. CRLF characters (\r, \n) are stripped from the value to prevent header injection.
func (*Request) WithHeaders ¶
WithHeaders merges the given map into the request headers. Later keys in the map override earlier ones; per-request headers always beat defaults. CRLF characters (\r, \n) are stripped from values to prevent header injection.
func (*Request) WithIdempotencyKey ¶
WithIdempotencyKey sets a custom X-Idempotency-Key header value. The key is reused unchanged across all retry attempts for this request. The value is sanitised to strip CR/LF characters.
func (*Request) WithJSON ¶
WithJSON marshals v to JSON, sets the body, and sets Content-Type to application/json. If marshalling fails the body is left unchanged.
func (*Request) WithMaxBodySize ¶ added in v0.1.1
WithMaxBodySize overrides the client-level [Config.MaxResponseBodyBytes] for this single request. Pass 0 to fall back to the client default (10 MB). Pass -1 to remove the limit entirely for this request.
func (*Request) WithMultipart ¶
func (r *Request) WithMultipart(fields []MultipartField) *Request
WithMultipart builds a multipart/form-data body from the provided fields. Supports plain form fields and file uploads with optional Content-Type overrides.
func (*Request) WithPathParam ¶
WithPathParam replaces a {key} placeholder in the URL template before sending. The value is percent-encoded automatically.
client.Get("/users/{id}").WithPathParam("id", "usr_42")
// → GET /users/usr_42
func (*Request) WithPathParams ¶
WithPathParams sets multiple URL path parameters at once.
client.Get("/orgs/{org}/users/{id}").WithPathParams(map[string]string{
"org": "alicorp",
"id": "usr_42",
})
func (*Request) WithPriority ¶ added in v0.1.25
WithPriority sets the priority level for this request. When the client has WithPriorityQueue enabled and the bulkhead is at capacity, requests with higher priority are dequeued before lower-priority ones. When WithPriorityQueue is not enabled, this has no effect. Defaults to PriorityNormal.
func (*Request) WithQueryParam ¶
WithQueryParam sets (or replaces) a single URL query parameter.
func (*Request) WithQueryParamValues ¶
WithQueryParamValues sets a multi-value query parameter, replacing any previously set values for the same key.
req.WithQueryParamValues("ids", []string{"1", "2", "3"})
// → ?ids=1&ids=2&ids=3
func (*Request) WithQueryParams ¶
WithQueryParams merges the given map into the URL query string. Later keys override earlier ones for the same name.
func (*Request) WithRequestID ¶
WithRequestID sets the X-Request-Id header. Useful for distributed tracing and log correlation when managing request identifiers outside of OTel.
func (*Request) WithTag ¶
WithTag attaches a client-side key/value label to the request. Tags are NOT sent as HTTP headers - they are visible to [Config.OnBeforeRequest] and [Config.OnAfterResponse] hooks for logging, metrics labelling, etc.
req.WithTag("operation", "CreateOrder").WithTag("team", "payments")
func (*Request) WithTimeout ¶
WithTimeout sets a per-request timeout that wraps the existing context. When the timeout fires, Client.Execute returns ErrTimeout.
func (*Request) WithUploadProgress ¶
func (r *Request) WithUploadProgress(fn ProgressFunc) *Request
WithUploadProgress registers a callback that is invoked periodically during request body upload. transferred is the number of bytes sent so far; total is the body size or -1 if unknown.
func (*Request) WithUserAgent ¶
WithUserAgent sets the User-Agent request header, overriding any client-level default set via WithDefaultHeaders.
type RequestSigner ¶ added in v0.1.12
RequestSigner signs outgoing HTTP requests. Implementations may add headers (e.g. Authorization, X-Signature), compute HMAC digests, or perform any mutation required by the target service's authentication scheme.
Sign is called once per attempt - including retries - after all default headers and the idempotency key have been applied, and immediately before the request is handed to the underlying transport.
Returning a non-nil error from Sign aborts the attempt and propagates as the Client.Execute error wrapped in "request signer: ...".
Built-in implementations: ext/sigv4 (AWS Signature Version 4). Third-party schemes (OAuth 1.0a, HMAC-SHA256, JWS) can be plugged in by implementing this interface and passing the value to WithSigner.
type RequestSignerFunc ¶ added in v0.1.12
RequestSignerFunc is a function that implements RequestSigner. It lets closures be used directly without defining a named type.
client := relay.New(
relay.WithSigner(relay.RequestSignerFunc(func(r *http.Request) error {
r.Header.Set("X-Api-Key", secret)
return nil
})),
)
func (RequestSignerFunc) Sign ¶ added in v0.1.12
func (f RequestSignerFunc) Sign(req *http.Request) error
Sign implements RequestSigner.
type RequestTiming ¶
type RequestTiming struct {
// DNSLookup is the time spent resolving the hostname.
DNSLookup time.Duration
// TCPConnect is the time to establish the TCP connection.
TCPConnect time.Duration
// TLSHandshake is the time to complete the TLS handshake (0 for plain HTTP).
TLSHandshake time.Duration
// TimeToFirstByte (TTFB) is the time from sending the request until the
// first byte of the response was received.
TimeToFirstByte time.Duration
// ContentTransfer is the time from first byte to the full body being read.
ContentTransfer time.Duration
// Total is the end-to-end wall-clock duration of the request.
Total time.Duration
}
RequestTiming holds a detailed breakdown of the time spent in each phase of an HTTP request.
type ResolutionResult ¶ added in v0.1.4
type ResolutionResult struct {
// URL is the final resolved URL as a string
URL string
// ParsedURL is the resolved URL parsed into a *url.URL structure
ParsedURL *url.URL
// Strategy indicates which normalisation strategy was used:
// - "Auto" for automatic detection
// - "RFC3986" for RFC 3986 resolution
// - "API" for safe string normalisation
Strategy string
// IsAPI indicates whether the base URL was detected as an API endpoint
IsAPI bool
}
ResolutionResult holds the result of URL resolution testing. It provides information about how the base URL and path were combined, which is useful for debugging URL resolution issues.
func ResolveTest ¶ added in v0.1.4
func ResolveTest(baseURL string, relativePath string, config *Config) *ResolutionResult
ResolveTest provides a way to test URL resolution without making an HTTP request. It takes a base URL, a relative path, and a config, then returns the resolved URL and information about how it was resolved. This is useful for debugging URL resolution behaviour and understanding which normalisation strategy is used.
Example:
config := relay.New().Config()
result := relay.ResolveTest("http://api.example.com/v1", "Products", config)
fmt.Println(result.URL) // "http://api.example.com/v1/Products"
fmt.Println(result.Strategy) // "Auto" or "RFC3986" or "API"
fmt.Println(result.IsAPI) // true (detected API pattern)
type Response ¶
type Response struct {
StatusCode int
Status string
Headers http.Header
Truncated bool // true when body was cut at MaxResponseBodyBytes
RedirectCount int // number of redirects followed to reach this response
Timing RequestTiming // per-phase timing breakdown (DNS, TCP, TLS, TTFB, ...)
// contains filtered or unexported fields
}
Response is a fully buffered HTTP response. The body has been read and closed by the time Response is returned from Execute. Use ExecuteStream for payloads that should not be buffered entirely in memory.
func ExecuteAs ¶
ExecuteAs executes req and deserialises the response body into a value of type T. When a WithResponseDecoder is configured on the client it is used; otherwise Decode falls back to JSON for application/json content and XML for application/xml. It is equivalent to calling Client.Execute followed by Response.Decode but avoids the need for an explicit interface{} target.
Example:
type User struct { ID int; Name string }
user, resp, err := relay.ExecuteAs[User](client, client.Get("/users/1"))
func (*Response) AsHTTPError ¶
AsHTTPError returns an *HTTPError for 4xx/5xx responses, or nil for success. Use this to convert an HTTP error status into a Go error.
func (*Response) Body ¶
Body returns the full response body as a byte slice. The slice is owned by Response; callers must not modify it.
func (*Response) BodyReader ¶
BodyReader returns a new io.Reader positioned at the start of the buffered body. Each call returns an independent reader; the underlying bytes are shared.
func (*Response) Bytes ¶ added in v0.1.16
Bytes returns a copy of the raw response body bytes. It is equivalent to Response.Body and is provided for API discoverability.
func (*Response) ContentType ¶
ContentType returns the media type from the Content-Type header, stripped of any parameters (e.g. "application/json" from "application/json; charset=utf-8").
func (*Response) Cookies ¶
Cookies parses and returns all cookies set by the server via Set-Cookie headers.
func (*Response) Decode ¶ added in v0.1.12
Decode deserialises the response body into v. When a WithResponseDecoder has been configured on the client, it is called with the response Content-Type header and the body bytes. Otherwise Decode falls back to JSON for application/json content and XML for application/xml content.
Decode is used internally by ExecuteAs to allow pluggable decoders (e.g. Protocol Buffers, MessagePack) without changing call sites.
func (*Response) IsClientError ¶
IsClientError reports whether the status code is 4xx.
func (*Response) IsRedirect ¶
IsRedirect reports whether the status code is 3xx.
func (*Response) IsServerError ¶
IsServerError reports whether the status code is 5xx.
func (*Response) IsTruncated ¶
IsTruncated reports whether the body was cut at MaxResponseBodyBytes.
func (*Response) Location ¶
Location returns the value of the Location response header, or "" if absent. Useful when inspecting redirects with redirect-following disabled.
func (*Response) Raw ¶
Raw returns the underlying *http.Response. The response body has already been consumed; use Body, String, or JSON to access the buffered bytes.
func (*Response) RedirectChain ¶ added in v0.1.16
func (r *Response) RedirectChain() []RedirectInfo
RedirectChain returns the sequence of redirects followed to reach this response, in order. Each entry records the From URL, the To URL, and the HTTP status code of the redirect response. The slice is empty when no redirects were followed. It is useful for debugging auth/SSO flows that involve multiple redirect hops.
func (*Response) Text ¶ added in v0.1.16
Text returns the response body decoded as a UTF-8 string. It is equivalent to Response.String and is provided for API discoverability.
func (*Response) WasRedirected ¶
WasRedirected reports whether at least one redirect was followed.
type RetryBudget ¶ added in v0.1.21
type RetryBudget struct {
// Ratio is the maximum fraction of requests that can be retried.
// E.g., 0.1 means at most 10% of requests in the window can be retried.
Ratio float64
// Window is the sliding window duration.
Window time.Duration
// MinRetry is the minimum number of retries always allowed regardless of ratio.
// Default 10.
MinRetry int
}
RetryBudget limits retries using a sliding window token bucket.
type RetryConfig ¶
type RetryConfig struct {
// MaxAttempts is the total number of tries, including the initial one.
// Set to 1 to disable retries.
MaxAttempts int
// InitialInterval is the base delay before the first retry.
InitialInterval time.Duration
// MaxInterval caps the computed backoff delay regardless of how many
// attempts have been made.
MaxInterval time.Duration
// Multiplier grows the interval on each successive attempt.
// 2.0 produces classic exponential backoff.
Multiplier float64
// RandomFactor adds ±jitter proportional to the computed interval.
// 0 disables jitter entirely; 0.5 adds up to ±50 % random jitter.
RandomFactor float64
// RetryableStatus is the set of HTTP status codes that trigger a retry.
// Defaults to [429, 500, 502, 503, 504].
RetryableStatus []int
// RetryIf is an optional predicate called when the built-in logic would
// retry. Returning false prevents the retry even when the status or error
// is normally retryable. Either argument may be nil depending on whether
// the trigger was an HTTP status code or a transport error.
//
// RetryIf: func(resp *http.Response, err error) bool {
// if resp != nil { return resp.StatusCode == 503 }
// return true
// }
RetryIf func(resp *http.Response, err error) bool
// OnRetry is an optional callback invoked before each retry sleep.
// attempt is 1-based (first retry = 1). Useful for structured logging.
// Either resp or err may be nil depending on the failure mode.
OnRetry func(attempt int, resp *http.Response, err error)
}
RetryConfig controls the retry and backoff behaviour of the client. The default policy retries on network errors and 5xx/429 responses using exponential backoff with full jitter.
type RotatingTokenProvider ¶ added in v0.1.33
type RotatingTokenProvider struct {
// contains filtered or unexported fields
}
RotatingTokenProvider caches a bearer token and refreshes it when within threshold of expiry. Concurrent callers block only during the refresh.
func NewRotatingTokenProvider ¶ added in v0.1.33
func NewRotatingTokenProvider( refresh func(ctx context.Context) (token string, expiry time.Time, err error), threshold time.Duration, ) *RotatingTokenProvider
NewRotatingTokenProvider returns a RotatingTokenProvider that calls refresh to obtain a bearer token and caches it until it is within threshold of expiry. The first call always triggers a refresh.
func (*RotatingTokenProvider) Credentials ¶ added in v0.1.33
func (r *RotatingTokenProvider) Credentials(ctx context.Context) (Credentials, error)
Credentials returns the cached token, refreshing it first if necessary.
type SRVBalancer ¶ added in v0.1.34
type SRVBalancer int
SRVBalancer selects from a list of SRV targets.
const ( SRVRoundRobin SRVBalancer = iota // rotate through targets in order SRVRandom // pick a random target each time SRVPriority // pick lowest-priority target (highest priority = lowest number) )
type SRVOption ¶ added in v0.1.34
type SRVOption func(*SRVResolver)
SRVOption configures an SRVResolver.
func WithSRVBalancer ¶ added in v0.1.34
func WithSRVBalancer(b SRVBalancer) SRVOption
WithSRVBalancer sets the load balancing strategy.
func WithSRVTTL ¶ added in v0.1.34
WithSRVTTL caches SRV lookup results for the given duration.
type SRVResolver ¶ added in v0.1.34
type SRVResolver struct {
// contains filtered or unexported fields
}
SRVResolver resolves DNS SRV records to select a backend host:port.
func NewSRVResolver ¶ added in v0.1.34
func NewSRVResolver(service, proto, name, scheme string, opts ...SRVOption) *SRVResolver
NewSRVResolver creates a resolver for the given SRV record.
- service: the service name (e.g. "http", "https", "grpc")
- proto: the protocol ("tcp" or "udp")
- name: the domain to query (e.g. "_http._tcp.myservice.example.com" or just "myservice.example.com")
- scheme: "http" or "https" for building URLs
func (*SRVResolver) Resolve ¶ added in v0.1.34
func (r *SRVResolver) Resolve(ctx context.Context) (string, error)
Resolve performs a DNS SRV lookup and returns the selected target as "host:port". Uses cached results if within TTL.
func (*SRVResolver) RoundTripperMiddleware ¶ added in v0.1.34
func (r *SRVResolver) RoundTripperMiddleware() func(http.RoundTripper) http.RoundTripper
RoundTripperMiddleware returns a relay-compatible middleware that rewrites the request host to the SRV-resolved address before each request.
type SSEClientConfig ¶ added in v0.1.25
type SSEClientConfig struct {
// MaxReconnects is the maximum number of reconnect attempts.
// 0 means unlimited. Default: 0.
MaxReconnects int
// ReconnectDelay is the base delay between reconnects.
// If the server sends a "retry" field, that takes precedence.
// Default: 3s.
ReconnectDelay time.Duration
// EventTypes filters which event types to deliver to the handler.
// Empty slice means deliver all events (default behaviour).
EventTypes []string
}
SSEClientConfig configures the SSE auto-reconnect behaviour.
type SSEEvent ¶ added in v0.1.1
type SSEEvent struct {
// ID is the event identifier set by the "id" field.
ID string
// Event is the event type set by the "event" field.
// Defaults to "message" when the field is absent, per the SSE spec.
Event string
// Data is the concatenated content of all "data" fields in the event,
// with a newline between each line.
Data string
// Retry is the reconnection time in milliseconds from the "retry" field.
// Zero means no retry directive was received.
Retry int
}
SSEEvent is a single Server-Sent Event as defined by the W3C EventSource specification (https://html.spec.whatwg.org/multipage/server-sent-events.html).
Fields not present in the stream are left at their zero values:
- Event defaults to "message" per spec when absent.
- Retry is 0 when no retry field was received.
type SSEFanOut ¶ added in v0.1.28
type SSEFanOut struct {
// contains filtered or unexported fields
}
SSEFanOut connects to a single SSE stream and multiplexes events to multiple concurrent subscribers. Only one upstream HTTP connection is maintained regardless of how many subscribers are active.
Slow subscribers whose channel buffer becomes full are automatically removed and their channels closed. All other subscribers continue receiving events unaffected.
Typical usage:
fo := relay.NewSSEFanOut(client, client.Get("/events"), 64)
ch1 := fo.Subscribe()
ch2 := fo.Subscribe()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() { _ = fo.Start(ctx) }()
for ev := range ch1 {
fmt.Println(ev.Data)
}
func NewSSEFanOut ¶ added in v0.1.28
NewSSEFanOut creates a new fan-out multiplexer. The client and req define the upstream SSE source. bufferSize is the per-subscriber channel buffer; it defaults to 64 when 0.
func (*SSEFanOut) Start ¶ added in v0.1.28
Start begins consuming the upstream SSE stream and distributing events to all active subscribers. It blocks until ctx is cancelled, Stop is called, or the upstream returns a non-recoverable error. Temporary disconnections trigger automatic reconnects after a short delay.
All subscriber channels are closed when Start returns.
func (*SSEFanOut) Stop ¶ added in v0.1.28
func (f *SSEFanOut) Stop()
Stop signals the fan-out to halt and closes all subscriber channels. It is safe to call Stop concurrently with Start and from multiple goroutines; only the first call has any effect.
func (*SSEFanOut) Subscribe ¶ added in v0.1.28
Subscribe registers a new subscriber and returns a read-only channel that receives SSE events. The channel is closed when the fan-out stops or the subscriber is removed due to being slow.
func (*SSEFanOut) SubscriberCount ¶ added in v0.1.28
SubscriberCount returns the number of currently active subscribers.
func (*SSEFanOut) Unsubscribe ¶ added in v0.1.28
Unsubscribe removes a subscriber. The channel returned by Subscribe is closed immediately after removal.
type SSEHandler ¶ added in v0.1.1
SSEHandler is called for each fully-parsed SSEEvent. Return true to continue reading; return false to stop the stream and close the connection.
type SchemaValidator ¶ added in v0.1.35
type SchemaValidator interface {
// Validate checks the decoded value and returns an error if validation fails.
// The value is the result of json.Unmarshal into an interface{}.
Validate(v interface{}) error
}
SchemaValidator validates a decoded JSON response against constraints.
type StreamResponse ¶
type StreamResponse struct {
// StatusCode is the HTTP status code (e.g. 200).
StatusCode int
// Status is the human-readable status line (e.g. "200 OK").
Status string
// Headers contains all response headers as received from the server.
Headers http.Header
// Body is the live, unbuffered response body. The caller must read from it
// and eventually close it to release the underlying connection.
Body io.ReadCloser
}
StreamResponse holds a live HTTP response with an unconsumed body reader. Unlike Response, the body is NOT buffered in memory - it is streamed on demand. Use this for large payloads (file downloads, SSE, JSONL, etc.).
The caller MUST call Body.Close() when done to release the TCP connection back to the pool and free all associated resources. Forgetting to close the body causes connection and goroutine leaks.
func (*StreamResponse) ContentType ¶
func (s *StreamResponse) ContentType() string
ContentType returns the Content-Type response header value.
func (*StreamResponse) Header ¶
func (s *StreamResponse) Header(key string) string
Header returns the value of the named response header.
func (*StreamResponse) IsClientError ¶
func (s *StreamResponse) IsClientError() bool
IsClientError reports whether the status code is 4xx.
func (*StreamResponse) IsError ¶
func (s *StreamResponse) IsError() bool
IsError reports whether the status code is 4xx or 5xx.
func (*StreamResponse) IsServerError ¶
func (s *StreamResponse) IsServerError() bool
IsServerError reports whether the status code is 5xx.
func (*StreamResponse) IsSuccess ¶
func (s *StreamResponse) IsSuccess() bool
IsSuccess reports whether the status code is 2xx.
type StructValidator ¶ added in v0.1.35
type StructValidator struct {
// contains filtered or unexported fields
}
StructValidator validates a response by attempting to decode it into the provided struct type and checking required fields via struct tags.
Usage:
type MyResponse struct {
ID int `json:"id" validate:"required"`
Name string `json:"name" validate:"required,min=1"`
}
relay.WithResponseValidator(relay.NewStructValidator(MyResponse{}))
func NewStructValidator ¶ added in v0.1.35
func NewStructValidator(prototype interface{}) *StructValidator
NewStructValidator creates a StructValidator using the given struct as the validation template. prototype must be a struct value or pointer to struct.
func (*StructValidator) Validate ¶ added in v0.1.35
func (s *StructValidator) Validate(v interface{}) error
Validate decodes v (which must be a map[string]interface{}) into the prototype struct and checks validate tags:
- "required": field must be non-zero
- "min=N": string min length / number min value
- "max=N": string max length / number max value
type URLNormalisationMode ¶ added in v0.1.4
type URLNormalisationMode int
URLNormalisationMode controls how [Config.BaseURL] is resolved against request paths in [Request.build]. Each mode has different characteristics regarding RFC 3986 compliance, zero-allocation, and API path preservation.
The default mode (NormalisationAuto) provides intelligent automatic detection, choosing the best strategy based on whether the base URL appears to be an API endpoint with path components.
const ( // NormalisationAuto (default) uses intelligent detection: RFC 3986 for // host-only URLs (e.g., http://api.com), safe string normalisation for // APIs with path components (e.g., http://api.com/v1). Zero configuration // required; automatically handles versioned APIs, OData, GraphQL, etc. NormalisationAuto URLNormalisationMode = iota // NormalisationRFC3986 forces RFC 3986 URL resolution via // url.ResolveReference(). Provides zero allocations but breaks API URLs // with path components (e.g., http://api.com/v1 + Products becomes // http://api.com/Products, losing the /v1 segment). Use this only if // you have host-only base URLs and need maximum performance. NormalisationRFC3986 // NormalisationAPI forces safe string normalisation for all URLs, // preserving base path components by concatenation instead of RFC 3986 // resolution. Slightly higher allocations than RFC 3986 but works correctly // for all API patterns. Use this if you want guaranteed API path preservation. NormalisationAPI )
func (URLNormalisationMode) String ¶ added in v0.1.4
func (m URLNormalisationMode) String() string
String returns a human-readable name for the normalisation mode.
type ValidationError ¶ added in v0.1.35
ValidationError is returned when the response body fails schema validation.
func (*ValidationError) Error ¶ added in v0.1.35
func (e *ValidationError) Error() string
type WSConn ¶ added in v0.1.19
type WSConn struct {
// contains filtered or unexported fields
}
WSConn is an active WebSocket connection created by Client.ExecuteWebSocket. It reuses the relay client's TLS configuration, default headers, and request signer so auth is consistent across HTTP and WebSocket calls.
WSConn is not safe for concurrent reads or concurrent writes, but a read and a write may proceed simultaneously from different goroutines.
Source Files
¶
- adaptive_timeout.go
- async.go
- auth_digest.go
- batch.go
- cache.go
- circuitbreaker.go
- classification.go
- client.go
- coalesce.go
- compress.go
- config.go
- credentials.go
- dns_cache.go
- doc.go
- errors.go
- generics.go
- har.go
- healthcheck.go
- hedging.go
- idempotency.go
- loadbalancer.go
- logger.go
- logging_middleware.go
- longpoll.go
- pagination.go
- path_builder.go
- priority.go
- progress.go
- push.go
- ratelimit.go
- request.go
- resolve.go
- response.go
- retry.go
- retry_budget.go
- schema.go
- signer.go
- singleflight.go
- srv_discovery.go
- sse.go
- sse_fanout.go
- stream.go
- timing.go
- tls.go
- tls_pinning.go
- tls_watcher.go
- transport.go
- transport_adapters.go
- unix_transport.go
- util.go
- websocket.go
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
relay
command
Package main implements a feature-rich HTTP client powered by the relay library, exposing retry, circuit-breaker, rate-limit, signing, streaming, download/upload and timing capabilities from the command line.
|
Package main implements a feature-rich HTTP client powered by the relay library, exposing retry, circuit-breaker, rate-limit, signing, streaming, download/upload and timing capabilities from the command line. |
|
relay-bench
command
Package main implements an HTTP load-testing tool powered by relay, demonstrating concurrent execution, connection pooling, rate limiting, and circuit-breaker observation.
|
Package main implements an HTTP load-testing tool powered by relay, demonstrating concurrent execution, connection pooling, rate limiting, and circuit-breaker observation. |
|
relay-gen
command
Package main implements relay-gen, an OpenAPI 3.x client code generator that produces type-safe Go clients using the relay library.
|
Package main implements relay-gen, an OpenAPI 3.x client code generator that produces type-safe Go clients using the relay library. |
|
relay-probe
command
Package main implements a multi-endpoint health probe powered by relay, demonstrating retry, circuit-breaker, and periodic monitoring capabilities.
|
Package main implements a multi-endpoint health probe powered by relay, demonstrating retry, circuit-breaker, and periodic monitoring capabilities. |
|
ext
|
|
|
breaker/gobreaker
module
|
|
|
brotli
module
|
|
|
cache/lru
module
|
|
|
cache/twolevel
module
|
|
|
chaos
module
|
|
|
compress
module
|
|
|
graphql
module
|
|
|
grpc
module
|
|
|
http3
module
|
|
|
jitterbug
module
|
|
|
logrus
module
|
|
|
memcached
module
|
|
|
metrics
module
|
|
|
mock
module
|
|
|
oauth
module
|
|
|
oidc
module
|
|
|
openapi
module
|
|
|
prometheus
module
|
|
|
ratelimit/distributed
module
|
|
|
redis
module
|
|
|
sentry
module
|
|
|
sigv4
module
|
|
|
slog
module
|
|
|
tracing
module
|
|
|
vcr
module
|
|
|
websocket
module
|
|
|
zap
module
|
|
|
zerolog
module
|
|
|
internal
|
|
|
backoff
Package backoff provides exponential backoff with full jitter for retry logic.
|
Package backoff provides exponential backoff with full jitter for retry logic. |
|
pool
Package pool provides reusable byte buffer pools to reduce GC pressure.
|
Package pool provides reusable byte buffer pools to reduce GC pressure. |
|
Package testutil provides test helpers for code that uses the relay HTTP client.
|
Package testutil provides test helpers for code that uses the relay HTTP client. |