integration

package
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Jul 28, 2026 License: MIT Imports: 19 Imported by: 0

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

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

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

func NewCaller(baseURL string) *Caller

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

func (c *Caller) Delete(ctx context.Context, path string, body, out any) error

Delete performs a DELETE request. body may be nil. out may be nil.

func (*Caller) Get

func (c *Caller) Get(ctx context.Context, path string, params url.Values, out any) error

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.

func (*Caller) Post

func (c *Caller) Post(ctx context.Context, path string, body, out any) error

Post performs a POST request with body JSON-encoded as the request body and decodes the JSON response into out. Pass out=nil to discard the response.

func (*Caller) Put

func (c *Caller) Put(ctx context.Context, path string, body, out any) error

Put performs a PUT request. Same semantics as Post.

type ErrHTTPStatus

type ErrHTTPStatus struct {
	StatusCode int
	Status     string
	Body       map[string]any
	RawBody    string
}

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)

func (*Poller) Start

func (p *Poller) Start(ctx context.Context) error

Start blocks until ctx is cancelled, invoking Fn on every Interval (and optionally immediately when RunOnStart is true). Returns ctx.Err() once the loop exits, or an error when configuration is invalid.

type WebhookHandler

type WebhookHandler func(w http.ResponseWriter, r *http.Request, body []byte) error

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:

  1. Reads at most MaxBodyBytes from the body, rejecting overflow with 413.
  2. Computes HMAC over the raw body using Secret + Algorithm.
  3. Compares the result to the value in HeaderKey (constant-time).
  4. Applies optional timestamp-window and replay checks.
  5. Looks up handlers[event] using EventHeaderKey.
  6. 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

type WebhookReplayCheck func(ctx context.Context, r *http.Request, body []byte) error

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.

Jump to

Keyboard shortcuts

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