Documentation
¶
Overview ¶
Package integration provides building blocks for outbound integrations: HTTP callers with retry/backoff, periodic pollers, and signed-webhook receivers. It's deliberately small — the framework owns the request pipeline, not the integration patterns that sit beside it.
Index ¶
- Constants
- Variables
- type Caller
- func (c *Caller) Delete(ctx context.Context, path string, body, out any) error
- func (c *Caller) Get(ctx context.Context, path string, params url.Values, out any) error
- func (c *Caller) Post(ctx context.Context, path string, body, out any) error
- func (c *Caller) Put(ctx context.Context, path string, body, out any) error
- type ErrHTTPStatus
- type Poller
- type WebhookHandler
- type WebhookReceiver
- type WebhookReplayCheck
Constants ¶
const ( // DefaultCallerTimeout bounds each outbound attempt. DefaultCallerTimeout = 10 * time.Second // DefaultCallerMaxRetry is the number of attempts after the first. DefaultCallerMaxRetry = 3 // DefaultCallerMaxResponseBytes bounds response bodies, including errors. DefaultCallerMaxResponseBytes int64 = 4 << 20 )
Variables ¶
var ( // ErrResponseTooLarge means an upstream response exceeded MaxResponseBytes. ErrResponseTooLarge = errors.New("integration: response body too large") // ErrCrossOriginRedirect means the default redirect policy refused to send // the request, including its static custom headers, to another origin. ErrCrossOriginRedirect = errors.New("integration: cross-origin redirect refused") )
var ErrWebhookReplay = errors.New("integration: webhook replay")
ErrWebhookReplay is returned by WebhookReceiver.ReplayCheck when a signed request has already been accepted. The receiver maps it to HTTP 409 without invoking the event handler.
Functions ¶
This section is empty.
Types ¶
type Caller ¶
type Caller struct {
// BaseURL is prepended to every request path. Must not be empty.
BaseURL string
// Timeout is the per-attempt timeout. Zero means 10 seconds; a negative
// value explicitly disables the timeout.
Timeout time.Duration
// MaxRetry is the number of additional attempts after a failure. Zero means
// 3; a negative value explicitly disables retries. Network errors, 5xx,
// and 429 are retriable; other 4xx responses are final.
MaxRetry int
// BackoffFn maps the attempt number (1-based) to the sleep duration
// before the next try. The default is jittered exponential backoff capped
// at 2 seconds. A valid Retry-After response header takes precedence and
// is capped at 30 seconds.
BackoffFn func(attempt int) time.Duration
// MaxResponseBytes bounds every response body, including non-2xx bodies and
// responses whose decoded value is discarded. Zero means 4 MiB; a negative
// value explicitly disables the limit.
MaxResponseBytes int64
// Headers are added to every request before per-call headers.
Headers map[string]string
// HTTPClient lets callers inject a custom *http.Client (for proxying,
// tracing, mocking). Caller clones it before use. A nil CheckRedirect gets
// the safe same-origin policy; set an explicit CheckRedirect to override it.
// A zero client timeout receives Caller's resolved timeout.
HTTPClient *http.Client
}
Caller is a JSON-over-HTTP client with built-in retry/backoff. Configure it once at startup; reuse the same Caller for every outbound call to a given service.
The zero value is unusable — BaseURL is required. All other fields have sensible defaults: 10s timeout, 3 retries on 5xx/network errors, a 4 MiB response limit, same-origin redirects, and jittered exponential backoff. Pass `Headers` to attach static headers (Authorization, X-Api-Key) to every request.
func NewCaller ¶ added in v0.4.0
NewCaller returns a Caller with production-safe timeout, retry, and response size defaults. A struct literal receives the same defaults at call time when those fields are zero.
func (*Caller) Get ¶
Get performs a GET request to path with the supplied query params and decodes the JSON body into out. Pass out=nil to discard the body.
type ErrHTTPStatus ¶
ErrHTTPStatus is returned when the upstream responds with a non-2xx status. The body — if it parses as JSON — is decoded into Body; otherwise it's left as a raw string in RawBody.
func (*ErrHTTPStatus) Error ¶
func (e *ErrHTTPStatus) Error() string
func (*ErrHTTPStatus) IsRetriable ¶
func (e *ErrHTTPStatus) IsRetriable() bool
IsRetriable reports whether a status code is worth retrying. 5xx and 429 are retriable; 4xx (except 429) is final.
type Poller ¶
type Poller struct {
// Interval between tick starts. A tick that runs longer than Interval
// delays the next one — there is no overlap.
Interval time.Duration
// Fn is the work performed on each tick. Honour ctx — Poller cancels it
// when Start's context is cancelled.
Fn func(ctx context.Context) error
// RunOnStart fires Fn immediately when Start is called, before the first
// interval. Default false: the first tick happens after Interval.
RunOnStart bool
// Logger captures per-tick errors. Default slog.Default().
Logger *slog.Logger
}
Poller invokes Fn on a fixed interval until the supplied context is cancelled. Errors from Fn are logged via the configured Logger (default slog.Default) and the loop continues — Poller is intended for best-effort background work where one failed tick should not stop the schedule.
Example: pulling new fingerprint records from a wall terminal every 30 seconds.
p := &integration.Poller{
Interval: 30 * time.Second,
Fn: func(ctx context.Context) error {
return terminal.Sync(ctx)
},
}
go p.Start(ctx)
type WebhookHandler ¶
WebhookHandler processes a decoded webhook payload after signature verification succeeds. The raw body is supplied so handlers can decode it into a type appropriate for the event.
type WebhookReceiver ¶
type WebhookReceiver struct {
// Secret is the shared HMAC key. Required.
Secret string
// Algorithm selects the HMAC hash: "sha256" (default) or "sha512".
Algorithm string
// HeaderKey is the request header carrying the hex-encoded signature.
// Defaults to "X-Hub-Signature-256" (GitHub-style); set to whatever the
// upstream uses.
HeaderKey string
// EventHeaderKey is the request header naming the event type (the dispatch
// key into the handlers map). Defaults to "X-Event-Type".
EventHeaderKey string
// MaxBodyBytes caps the request body size. 0 means 1 MiB.
MaxBodyBytes int64
// TimestampHeaderKey enables timestamp-window validation. The named header
// must contain Unix seconds or an RFC3339 timestamp, and the signature must
// cover "<raw timestamp>.<raw body>" instead of only the body. Empty
// disables timestamp validation.
TimestampHeaderKey string
// TimestampTolerance is the maximum accepted clock skew in either
// direction when TimestampHeaderKey is set. Zero means 5 minutes.
TimestampTolerance time.Duration
// Clock supplies the current time for timestamp validation. Nil uses
// time.Now. It is primarily useful for deterministic tests.
Clock func() time.Time
// ReplayCheck optionally performs atomic event-ID deduplication after the
// signature and timestamp have been verified. Return ErrWebhookReplay for
// a duplicate delivery.
ReplayCheck WebhookReplayCheck
// Logger receives handler failures with their private diagnostic text.
// Defaults to slog.Default. Clients receive only a generic 500 response.
Logger *slog.Logger
}
WebhookReceiver verifies an HMAC signature on inbound webhook requests and dispatches to a per-event handler. Use it for payment-gateway / e-invoicing callbacks where the upstream signs each request with a shared secret.
func (*WebhookReceiver) Handler ¶
func (r *WebhookReceiver) Handler(handlers map[string]WebhookHandler) http.HandlerFunc
Handler returns an http.HandlerFunc that, on each request:
- Reads at most MaxBodyBytes from the body, rejecting overflow with 413.
- Computes HMAC over the raw body using Secret + Algorithm.
- Compares the result to the value in HeaderKey (constant-time).
- Applies optional timestamp-window and replay checks.
- Looks up handlers[event] using EventHeaderKey.
- Invokes the handler with the raw body.
Failures map to: 400 (read), 401 (signature/timestamp), 409 (replay), 413 (oversized body), 404 (no handler), or a generic 500 for a handler/replay backend error. Private diagnostics are written only to the configured Logger.
type WebhookReplayCheck ¶ added in v0.4.0
WebhookReplayCheck atomically checks and records a verified webhook request.
Implementations normally extract the provider's event ID from the signed body and claim it in a shared store with a TTL. The check runs only after the HMAC and optional timestamp have been verified. Return ErrWebhookReplay when the event was already claimed; other errors are logged and returned to the client as a generic 500.
The check must be atomic to stop two concurrent deliveries of the same event from both reaching the handler.