relay

package module
v0.4.1 Latest Latest
Warning

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

Go to latest
Published: Apr 27, 2026 License: MIT Imports: 46 Imported by: 0

README

Relay

A production-grade, declarative HTTP client for Go.

Go Version CI Tests Codecov CodeQL Trivy Release pkg.go.dev Go Report Card


Documentation · pkg.go.dev · Quick Start · Extensions · Tools · Changelog

Overview

Relay brings the ergonomics of Python's requests and the resilience of Resilience4j to Go. It provides a fluent, batteries-included API for building resilient HTTP clients: retries, circuit breaking, rate limiting, deduplication, adaptive timeouts, load balancing, and full observability - all composable via options.

The core module has zero external dependencies. Every integration (Redis, OTel, Prometheus, gRPC, slog, chaos, VCR, etc.) lives in its own optional extension module so you only pull in what you need.

go get github.com/jhonsferg/relay

Requires Go 1.24 or later. WASM (js/wasm) targets are supported.


Quick Start

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "github.com/jhonsferg/relay"
)

type Repo struct {
    ID   int    `json:"id"`
    Name string `json:"full_name"`
}

func main() {
    client := relay.New(
        relay.WithBaseURL("https://api.github.com"),
        relay.WithRetry(relay.RetryConfig{MaxAttempts: 3}),
        relay.WithTimeout(10*time.Second),
    )

    var repo Repo
    _, err := relay.Get[Repo](context.Background(), client, "/repos/golang/go", &repo)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(repo.Name)
}

Features

Core
Feature Description
Fluent request builder Chain .GET(), .POST(), .Header(), .Query(), .Body()
Retry & backoff Exponential backoff with jitter, configurable retry conditions
Circuit breaker Automatic open/half-open/closed state machine
Rate limiting Token bucket and sliding window algorithms
Request deduplication In-flight singleflight to collapse concurrent identical requests
Retry budget Sliding window budget to prevent retry storms
Client-side load balancer Round-robin and random strategies across backends
Adaptive timeout Percentile-based timeout derived from observed latency
Bulkhead isolation Concurrency limits per client or endpoint group
Request hedging Parallel speculative requests, use first response
Streaming io.Reader and channel-based streaming for large payloads
Hooks OnBeforeRequest / OnAfterResponse for auth, logging, metrics
Generic decode relay.Get[T], relay.Post[T] with zero-alloc JSON decoding
Error classification Distinguish transient / permanent / rate-limited errors
ETag & idempotency Built-in idempotency key generation and ETag support
TLS & certificates Dynamic cert reloading, mTLS, custom CA bundles
Auth & Credentials
Feature Description
Bearer / Basic auth WithBearerToken, WithBasicAuth options
HMAC-SHA256 signing HMACRequestSigner sets X-Timestamp + X-Signature automatically
Multi-signer NewMultiSigner chains multiple signers in order
Credential rotation RotatingTokenProvider refreshes tokens before expiry with a configurable threshold
Custom signer Implement the RequestSigner interface for any auth scheme
mTLS Mutual TLS with client certificates
Transport
Feature Description
Unix domain socket WithUnixSocket - connect to local services via socket path (Linux/macOS)
DNS SRV discovery WithSRVDiscovery - resolve service endpoints via DNS SRV records with TTL caching
HTTP/2 push promises WithHTTP2PushHandler - handle server push promises and cache pushed responses
WASM/js Builds on js/wasm; WithUnixSocket is a no-op on that target for portability
Compression
Feature Description
Gzip / Zstd WithCompression(relay.Gzip) or WithCompression(relay.Zstd) for response decompression
Request compression WithRequestCompression compresses outgoing request bodies
Dictionary Zstd ext/compress - ZstdDictionaryCompressor for pre-shared dictionary compression
Observability
Feature Description
HAR export HARRecorder captures all traffic in HAR format; HARRecorder.All() returns a Go 1.23 iter.Seq[HAREntry] iterator
OpenTelemetry ext/otel - unified tracing + metrics via WithOtel(tracer, meter)
Prometheus ext/prometheus - Prometheus metrics exporter
Validation
Feature Description
Response schema validation WithSchemaValidator - validate decoded responses against struct tags or a JSON Schema
Struct validator NewStructValidator - validates required fields and rules via struct tags
JSON Schema validator NewJSONSchemaValidator - validates against an inline JSON Schema definition

Full feature documentation: jhonsferg.github.io/relay


Extensions

Each extension is a standalone Go module - add only what you use:

Module Import path Description
ext/compress github.com/jhonsferg/relay/ext/compress Dictionary-based Zstd compression (ZstdDictionaryCompressor)
ext/oidc github.com/jhonsferg/relay/ext/oidc OIDC/JWT bearer token provider
ext/otel github.com/jhonsferg/relay/ext/otel OpenTelemetry tracing + metrics in one option (WithOtel)
ext/tracing github.com/jhonsferg/relay/ext/tracing OpenTelemetry distributed tracing (standalone)
ext/metrics github.com/jhonsferg/relay/ext/metrics OpenTelemetry metrics (standalone)
ext/prometheus github.com/jhonsferg/relay/ext/prometheus Prometheus metrics exporter
ext/slog github.com/jhonsferg/relay/ext/slog Structured logging via log/slog
ext/zap github.com/jhonsferg/relay/ext/zap Zap logging integration
ext/chaos github.com/jhonsferg/relay/ext/chaos Fault injection for resilience testing
ext/vcr github.com/jhonsferg/relay/ext/vcr HTTP cassette recording and playback
ext/mock github.com/jhonsferg/relay/ext/mock Mock transport for unit tests
ext/oauth github.com/jhonsferg/relay/ext/oauth OAuth2 token management
ext/sigv4 github.com/jhonsferg/relay/ext/sigv4 AWS SigV4 request signing
ext/openapi github.com/jhonsferg/relay/ext/openapi OpenAPI request/response validation
ext/redis github.com/jhonsferg/relay/ext/redis Redis-backed cache and rate limiting
ext/http3 github.com/jhonsferg/relay/ext/http3 HTTP/3 QUIC transport
ext/websocket github.com/jhonsferg/relay/ext/websocket WebSocket upgrade
ext/grpc github.com/jhonsferg/relay/ext/grpc gRPC bridge transport
ext/graphql github.com/jhonsferg/relay/ext/graphql GraphQL query support
ext/sentry github.com/jhonsferg/relay/ext/sentry Sentry error reporting
ext/brotli github.com/jhonsferg/relay/ext/brotli Brotli compression support
ext/breaker/gobreaker github.com/jhonsferg/relay/ext/breaker/gobreaker Circuit breaker backed by gobreaker
ext/cache/lru github.com/jhonsferg/relay/ext/cache/lru In-process LRU response cache
ext/cache/twolevel github.com/jhonsferg/relay/ext/cache/twolevel Two-level (L1+L2) response cache
ext/ratelimit/distributed github.com/jhonsferg/relay/ext/ratelimit/distributed Distributed rate limiting
ext/memcached github.com/jhonsferg/relay/ext/memcached Memcached-backed cache
ext/jitterbug github.com/jhonsferg/relay/ext/jitterbug Pluggable jitter strategies for retry backoff

Extension documentation: jhonsferg.github.io/relay/extensions


Tools

relay-gen - OpenAPI client generator

relay-gen reads an OpenAPI 3.x spec and generates a type-safe Go client using relay:

go install github.com/jhonsferg/relay/cmd/relay-gen@latest

relay-gen -spec openapi.json -pkg acme -out ./acme/client.go

The generated client exposes one method per operation with strongly-typed request/response structs and full relay middleware support.

relay-probe - health check CLI
go install github.com/jhonsferg/relay/cmd/relay-probe@latest
relay-probe https://api.example.com/health
relay-bench - micro-benchmarking harness
go install github.com/jhonsferg/relay/cmd/relay-bench@latest
relay-bench -url https://api.example.com/ping -n 1000

Unix Socket Transport

Connect to services exposed via Unix domain sockets (Linux/macOS):

client := relay.New(
    relay.WithBaseURL("http://localhost"),
    relay.WithUnixSocket("/var/run/myapp.sock"),
)
resp, err := client.Execute(relay.NewRequest().GET("/status"))

Note: On js/wasm targets WithUnixSocket is accepted but silently ignored, keeping call sites portable.


DNS SRV Discovery

Resolve backends dynamically from DNS SRV records:

resolver := relay.NewSRVResolver("myservice", "tcp", "example.com", "https",
    relay.WithSRVTTL(30*time.Second),
    relay.WithSRVBalancer(relay.SRVRoundRobin),
)
client := relay.New(relay.WithSRVDiscovery(resolver))

HTTP/2 Push Promises

Handle server-pushed resources and cache them for subsequent requests:

cache := relay.NewPushedResponseCache()
client := relay.New(
    relay.WithBaseURL("https://api.example.com"),
    relay.WithHTTP2PushHandler(cache),
)

HAR Recording

Capture all traffic in HAR format for debugging or test fixtures:

rec := relay.NewHARRecorder()
client := relay.New(relay.WithMiddleware(rec.Middleware()))

// iterate with a Go 1.23 range-over-func loop
for entry := range rec.All() {
    fmt.Println(entry.Request.URL, entry.Response.Status)
}

data, _ := rec.Export()
os.WriteFile("traffic.har", data, 0o644)

Request Authentication

HMAC-SHA256 signing - sets X-Timestamp and X-Signature headers automatically:

client := relay.New(
    relay.WithRequestSigner(&relay.HMACRequestSigner{Key: []byte(secret)}),
)

Rotating token provider - refreshes credentials before expiry:

provider := relay.NewRotatingTokenProvider(fetchTokenFunc, 5*time.Minute)
client := relay.New(relay.WithCredentialProvider(provider))

Multiple signers - chain signers in order with NewMultiSigner:

client := relay.New(
    relay.WithRequestSigner(relay.NewMultiSigner(
        &relay.HMACRequestSigner{Key: signingKey},
        relay.RequestSignerFunc(func(r *http.Request) error {
            r.Header.Set("X-Tenant", tenantID)
            return nil
        }),
    )),
)

Response Schema Validation

Validate decoded responses against struct tags or a JSON Schema:

// struct-tag validation
validator := relay.NewStructValidator(MyResponse{})
client := relay.New(relay.WithSchemaValidator(validator))

// JSON Schema validation
validator, err := relay.NewJSONSchemaValidator(`{"type":"object","required":["id"]}`)
client := relay.New(relay.WithSchemaValidator(validator))

CI/CD

Relay's CI pipeline runs across 6 OS/Go version combinations and includes:

  • Unit & integration tests - ci.yml
  • CodeQL static analysis - codeql.yml
  • Vulnerability scanning - Trivy (trivy.yml)
  • Benchmark regression detection (on-demand) - benchstat.yml compares benchmarks against master using benchstat; run manually via workflow_dispatch when needed
  • Benchmark dashboard (on-demand) - benchmark-dashboard.yml regenerates the 📊 live dashboard; triggered manually via workflow_dispatch

Testing

import "github.com/jhonsferg/relay/testutil"

srv := testutil.NewMockServer()
defer srv.Close()

srv.Enqueue(testutil.Response{Status: 200, Body: `{"id":1}`})

client := relay.New(relay.WithBaseURL(srv.URL))
resp, err := client.Execute(relay.NewRequest().GET("/items/1"))

Testing guide: jhonsferg.github.io/relay/guides/testing


Documentation

The full documentation is at jhonsferg.github.io/relay:


License

MIT - see LICENSE.

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:

Error handling:

Resilience options:

Auth:

Transport:

Caching:

Streaming & async:

Observability:

Hooks:

URL building:

## 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:

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

Constants

This section is empty.

Variables

View Source
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.

View Source
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.

View Source
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

func DecodeAs[T any](resp *Response) (T, error)

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

func DecodeJSON[T any](resp *Response) (T, error)

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

func DecodeXML[T any](resp *Response) (T, error)

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

func ExecuteAsStream[T any](c *Client, req *Request, handler func(T) bool) error

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

func IsCircuitOpen(err error) bool

IsCircuitOpen reports whether the error was caused by the circuit breaker being in the Open state (i.e. ErrCircuitOpen).

func IsPermanentError

func IsPermanentError(err error, resp *Response) bool

IsPermanentError reports whether the error will not succeed on retry (e.g. 400 Bad Request, 401 Unauthorised, 403 Forbidden, 404 Not Found).

func IsRateLimitedError

func IsRateLimitedError(err error, resp *Response) bool

IsRateLimitedError reports whether the error is a 429 Too Many Requests.

func IsRetryableError added in v0.1.16

func IsRetryableError(err error, resp *Response) bool

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

func IsTimeout(err error) bool

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

func IsTransientError(err error, resp *Response) bool

IsTransientError reports whether the error may succeed on a subsequent call.

func NormaliseBaseURL added in v0.1.4

func NormaliseBaseURL(urlStr string) string

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:

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

type AsyncResult struct {
	Response *Response
	Err      error
}

AsyncResult holds the outcome of an asynchronous HTTP request.

type BasicAuthCreds added in v0.1.33

type BasicAuthCreds struct {
	Username string
	Password string
}

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

type BeforeRedirectHookFunc func(req *http.Request, via []*http.Request) error

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

func New(opts ...Option) *Client

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

func (c *Client) BaseURL() string

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) Delete

func (c *Client) Delete(url string) *Request

Delete returns a DELETE request builder for the given URL.

func (*Client) Execute

func (c *Client) Execute(req *Request) (resp *Response, err error)

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

func (c *Client) ExecuteAsyncCallback(req *Request, onSuccess func(*Response), onError func(error))

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

func (c *Client) ExecuteJSON(req *Request, out interface{}) (*Response, error)

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

func (c *Client) ExecuteWebSocket(ctx context.Context, req *Request) (*WSConn, error)

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) Get

func (c *Client) Get(url string) *Request

Get returns a GET request builder for the given URL.

func (*Client) Head

func (c *Client) Head(url string) *Request

Head returns a HEAD request builder for the given URL.

func (*Client) IsHealthy

func (c *Client) IsHealthy() bool

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) Options

func (c *Client) Options(url string) *Request

Options returns an OPTIONS request builder for the given URL.

func (*Client) Paginate added in v0.1.17

func (c *Client) Paginate(ctx context.Context, req *Request, fn PageFunc) error

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) Patch

func (c *Client) Patch(url string) *Request

Patch returns a PATCH request builder for the given URL.

func (*Client) Post

func (c *Client) Post(url string) *Request

Post returns a POST request builder for the given URL.

func (*Client) Put

func (c *Client) Put(url string) *Request

Put returns a PUT request builder for the given URL.

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

func (c *Client) Shutdown(ctx context.Context) error

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.

func (*Client) With

func (c *Client) With(opts ...Option) *Client

With returns a new Client that inherits all settings from the current client and applies the given options on top. The original client is not modified.

Cookie jars are intentionally shared between parent and child (same session). Pass WithCookieJar(nil) in the child to detach.

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

type HARCreator struct {
	Name    string `json:"name"`
	Version string `json:"version"`
}

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

type HARNameVal struct {
	Name  string `json:"name"`
	Value string `json:"value"`
}

HARNameVal is a key/value pair used for headers and query params.

type HARPostData

type HARPostData struct {
	MimeType string `json:"mimeType"`
	Text     string `json:"text"`
}

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.

func (*HARRecorder) Reset

func (r *HARRecorder) Reset()

Reset clears all recorded entries.

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.

func (*HMACRequestSigner) Sign added in v0.1.30

func (h *HMACRequestSigner) Sign(req *http.Request) error

Sign computes and sets the X-Timestamp and signature headers on req.

type HTTPError

type HTTPError struct {
	StatusCode int
	Status     string
	Body       []byte
}

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

func IsHTTPError(err error) (*HTTPError, bool)

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.

func (*HTTPError) Error

func (e *HTTPError) Error() string

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

func NewDefaultLogger(level slog.Level) Logger

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

func SlogAdapter(l *slog.Logger) Logger

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.

func (*MultiSigner) Sign added in v0.1.30

func (m *MultiSigner) Sign(req *http.Request) error

Sign applies each signer in order. It stops and returns an error as soon as any signer fails.

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

type NextPageFunc func(resp *Response) string

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

type OnErrorHookFunc func(ctx context.Context, req *Request, err error)

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

func WithAutoNormaliseURL(enable bool) Option

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

func WithBaseURL(urlStr string) Option

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

func WithCertificatePinning(pins []string) Option

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

func WithClientCert(certFile, keyFile string) Option

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

func WithClientCertPEM(certPEM, keyPEM []byte) Option

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

func WithConnectionPool(maxIdle, maxIdlePerHost, maxPerHost int) Option

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

func WithCookieJar(jar http.CookieJar) Option

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

func WithCustomDialer(dialer *net.Dialer) Option

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

func WithDNSCache(ttl time.Duration) Option

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

func WithDNSOverride(hosts map[string]string) Option

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

func WithDefaultAccept(accept string) Option

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

func WithDefaultHeaders(headers map[string]string) Option

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

func WithDialKeepAlive(d time.Duration) Option

WithDialKeepAlive sets the interval between TCP keep-alive probes on active connections.

func WithDialTimeout

func WithDialTimeout(d time.Duration) Option

WithDialTimeout sets the maximum time allowed for the TCP dial to complete.

func WithDigestAuth

func WithDigestAuth(username, password string) Option

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

func WithDynamicTLSCert(certFile, keyFile string, interval time.Duration) Option

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

func WithErrorDecoder(fn func(statusCode int, body []byte) error) Option

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

func WithExpectContinueTimeout(d time.Duration) Option

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

func WithHealthCheck(url string, interval, timeout time.Duration, expectedStatus int) Option

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

func WithHedging(after time.Duration) Option

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

func WithHedgingN(after time.Duration, maxAttempts int) Option

WithHedgingN enables request hedging with up to maxAttempts concurrent duplicates. after is the delay between launching each attempt.

func WithIdleConnTimeout

func WithIdleConnTimeout(d time.Duration) Option

WithIdleConnTimeout sets how long an idle keep-alive connection remains open before being evicted from the pool.

func WithInMemoryCache

func WithInMemoryCache(maxEntries int) Option

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

func WithLogger(l Logger) Option

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

func WithMaxConcurrentRequests(n int) Option

WithMaxConcurrentRequests sets the maximum number of in-flight requests allowed simultaneously (bulkhead). Zero or negative means no limit.

func WithMaxRedirects

func WithMaxRedirects(n int) Option

WithMaxRedirects sets the maximum number of redirects to follow automatically. Set to 0 to disable redirect following entirely.

func WithMaxResponseBodyBytes

func WithMaxResponseBodyBytes(n int64) Option

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

func WithOnAfterResponse(hook func(context.Context, *Response) error) Option

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

func WithOnBeforeRequest(hook func(context.Context, *Request) error) Option

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

func WithOnRetry(fn func(attempt int, resp *http.Response, err error)) Option

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

func WithProxy(proxyURL string) Option

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

func WithRateLimit(rps float64, burst int) Option

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

func WithRequestLogger(logger Logger) Option

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

func WithResponseDecoder(fn func(contentType string, body []byte, v any) error) Option

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

func WithResponseHeaderTimeout(d time.Duration) Option

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

func WithRetryIf(fn func(resp *http.Response, err error) bool) Option

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

func WithRootCA(caPEM []byte) Option

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

func WithTLSConfig(tlsCfg *tls.Config) Option

WithTLSConfig replaces the default TLS configuration. The default enforces TLS 1.2 as the minimum protocol version.

func WithTLSHandshakeTimeout added in v0.1.17

func WithTLSHandshakeTimeout(d time.Duration) Option

WithTLSHandshakeTimeout sets the deadline for completing the TLS handshake.

func WithTimeout

func WithTimeout(d time.Duration) Option

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:

  • NormalisationAuto (default): Detects API patterns and uses appropriate strategy (RFC 3986 for host-only, safe normalisation for APIs).
  • NormalisationRFC3986: Forces RFC 3986 resolution (zero-alloc, breaks APIs).
  • NormalisationAPI: Forces safe string normalisation (preserves all paths).

func WithUnixSocket added in v0.1.29

func WithUnixSocket(socketPath string) Option

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

func WithWebSocketDialTimeout(d time.Duration) Option

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

type PageFunc func(resp *Response) (more bool, err error)

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

type PushPromiseHandler func(pushedURL string, pushedResp *http.Response)

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.

func (*PushedResponseCache) Load added in v0.1.39

func (c *PushedResponseCache) Load(url string) (*http.Response, bool)

Load retrieves and removes the pushed response for url. Returns nil, false when no entry exists.

func (*PushedResponseCache) Store added in v0.1.39

func (c *PushedResponseCache) Store(url string, resp *http.Response)

Store stores a pushed response keyed by its URL. Any previously stored response for the same URL is silently replaced.

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

func (r *Request) Clone() *Request

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

func (r *Request) Method() string

Method returns the HTTP verb for this request (e.g. "GET", "POST").

func (*Request) Priority added in v0.1.25

func (r *Request) Priority() Priority

Priority returns the priority level set via Request.WithPriority. Returns PriorityNormal if not explicitly set.

func (*Request) Tag

func (r *Request) Tag(key string) string

Tag returns the value of a tag previously set via Request.WithTag, or "" if the tag is absent.

func (*Request) Tags

func (r *Request) Tags() map[string]string

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

func (r *Request) URL() string

URL returns the raw URL string as provided to the builder, before path parameters are substituted or query parameters are appended.

func (*Request) WithAPIKey

func (r *Request) WithAPIKey(headerName, apiKey string) *Request

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

func (r *Request) WithAccept(accept string) *Request

WithAccept sets the Accept request header.

func (*Request) WithBasicAuth

func (r *Request) WithBasicAuth(username, password string) *Request

WithBasicAuth sets the Authorization header to the RFC 7617 Basic credential for the given username and password.

func (*Request) WithBearerToken

func (r *Request) WithBearerToken(token string) *Request

WithBearerToken sets the Authorization header to "Bearer <token>".

func (*Request) WithBody

func (r *Request) WithBody(body []byte) *Request

WithBody sets the raw request body bytes. The caller is responsible for also setting Content-Type via Request.WithContentType.

func (*Request) WithBodyReader

func (r *Request) WithBodyReader(reader io.Reader) *Request

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

func (r *Request) WithContentType(ct string) *Request

WithContentType sets the Content-Type request header.

func (*Request) WithContext

func (r *Request) WithContext(ctx context.Context) *Request

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

func (r *Request) WithDeduplication(enabled ...bool) *Request

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

func (r *Request) WithFormData(data map[string]string) *Request

WithFormData URL-encodes data and sets Content-Type to application/x-www-form-urlencoded.

func (*Request) WithHeader

func (r *Request) WithHeader(key, value string) *Request

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

func (r *Request) WithHeaders(headers map[string]string) *Request

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

func (r *Request) WithIdempotencyKey(key string) *Request

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

func (r *Request) WithJSON(v interface{}) *Request

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

func (r *Request) WithMaxBodySize(n int64) *Request

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

func (r *Request) WithPathParam(key, value string) *Request

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

func (r *Request) WithPathParams(params map[string]string) *Request

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

func (r *Request) WithPriority(p Priority) *Request

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

func (r *Request) WithQueryParam(key, value string) *Request

WithQueryParam sets (or replaces) a single URL query parameter.

func (*Request) WithQueryParamValues

func (r *Request) WithQueryParamValues(key string, values []string) *Request

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

func (r *Request) WithQueryParams(params map[string]string) *Request

WithQueryParams merges the given map into the URL query string. Later keys override earlier ones for the same name.

func (*Request) WithRequestID

func (r *Request) WithRequestID(id string) *Request

WithRequestID sets the X-Request-Id header. Useful for distributed tracing and log correlation when managing request identifiers outside of OTel.

func (*Request) WithTag

func (r *Request) WithTag(key, value string) *Request

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

func (r *Request) WithTimeout(d time.Duration) *Request

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

func (r *Request) WithUserAgent(ua string) *Request

WithUserAgent sets the User-Agent request header, overriding any client-level default set via WithDefaultHeaders.

type RequestSigner added in v0.1.12

type RequestSigner interface {
	Sign(req *http.Request) error
}

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

type RequestSignerFunc func(req *http.Request) error

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

func ExecuteAs[T any](c *Client, req *Request) (T, *Response, error)

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

func (r *Response) AsHTTPError() *HTTPError

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

func (r *Response) Body() []byte

Body returns the full response body as a byte slice. The slice is owned by Response; callers must not modify it.

func (*Response) BodyReader

func (r *Response) BodyReader() io.Reader

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

func (r *Response) Bytes() []byte

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

func (r *Response) ContentType() string

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

func (r *Response) Cookies() []*http.Cookie

Cookies parses and returns all cookies set by the server via Set-Cookie headers.

func (*Response) Decode added in v0.1.12

func (r *Response) Decode(v any) error

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) Header

func (r *Response) Header(key string) string

Header returns the value of the named response header.

func (*Response) IsClientError

func (r *Response) IsClientError() bool

IsClientError reports whether the status code is 4xx.

func (*Response) IsError

func (r *Response) IsError() bool

IsError reports whether the status code is 4xx or 5xx.

func (*Response) IsRedirect

func (r *Response) IsRedirect() bool

IsRedirect reports whether the status code is 3xx.

func (*Response) IsServerError

func (r *Response) IsServerError() bool

IsServerError reports whether the status code is 5xx.

func (*Response) IsSuccess

func (r *Response) IsSuccess() bool

IsSuccess reports whether the status code is 2xx.

func (*Response) IsTruncated

func (r *Response) IsTruncated() bool

IsTruncated reports whether the body was cut at MaxResponseBodyBytes.

func (*Response) JSON

func (r *Response) JSON(v interface{}) error

JSON unmarshals the response body into v using encoding/json.

func (*Response) Location

func (r *Response) Location() string

Location returns the value of the Location response header, or "" if absent. Useful when inspecting redirects with redirect-following disabled.

func (*Response) Raw

func (r *Response) Raw() *http.Response

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) String

func (r *Response) String() string

String returns the response body decoded as a UTF-8 string.

func (*Response) Text added in v0.1.16

func (r *Response) Text() string

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

func (r *Response) WasRedirected() bool

WasRedirected reports whether at least one redirect was followed.

func (*Response) XML

func (r *Response) XML(v interface{}) error

XML unmarshals the response body into v using encoding/xml.

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

func WithSRVTTL(d time.Duration) SRVOption

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

func NewSSEFanOut(client *Client, req *Request, bufferSize int) *SSEFanOut

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

func (f *SSEFanOut) Start(ctx context.Context) error

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

func (f *SSEFanOut) Subscribe() <-chan SSEEvent

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

func (f *SSEFanOut) SubscriberCount() int

SubscriberCount returns the number of currently active subscribers.

func (*SSEFanOut) Unsubscribe added in v0.1.28

func (f *SSEFanOut) Unsubscribe(ch <-chan SSEEvent)

Unsubscribe removes a subscriber. The channel returned by Subscribe is closed immediately after removal.

type SSEHandler added in v0.1.1

type SSEHandler func(event SSEEvent) bool

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

type ValidationError struct {
	Field   string
	Message string
}

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.

func (*WSConn) Close added in v0.1.19

func (c *WSConn) Close() error

Close closes the underlying WebSocket connection.

func (*WSConn) Read added in v0.1.19

func (c *WSConn) Read(ctx context.Context) ([]byte, error)

Read reads the next binary or text message from the peer. It blocks until a message arrives, the connection is closed, or ctx is cancelled.

func (*WSConn) Write added in v0.1.19

func (c *WSConn) Write(ctx context.Context, data []byte) error

Write sends data as a binary message to the peer.

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
brotli module
cache/lru 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
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.

Jump to

Keyboard shortcuts

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