Documentation
¶
Overview ¶
Package webhook delivers domain events to customer-configured HTTP endpoints. It composes maintained libraries rather than hand-rolling the security-critical parts: doyensec/safeurl for the SSRF-hardened HTTP client and the standard-webhooks library for interoperable HMAC signatures.
Index ¶
- Constants
- func HookMaxAttempts() int
- func HookNextRetry(attemptedAt time.Time, attempt int) time.Time
- func HookRetryDelay(attempt int) time.Duration
- func HookSnooze(retryAfter string, now time.Time) time.Duration
- func ParseRetryAfter(value string, now time.Time) (wait time.Duration, ok bool)
- func Post(ctx context.Context, client Doer, c Call) error
- func Retryable(sendErr error) bool
- func Send(ctx context.Context, client Doer, url, secret, eventName, deliveryID string, ...) error
- type Allowlist
- type Call
- type DeliveryError
- type Doer
- type ErrorKind
- type RateLimitVerdict
Constants ¶
const ( HookRetryHorizon = 72 * time.Hour HookRetryFirstDelay = 30 * time.Second HookRetryMaxDelay = 6 * time.Hour )
The retry policy of an outbound call to a third-party hook (ADR 0039): a Webhook delivery and, through the same functions, an HTTP step. A receiver that is down for a weekend still gets the event: retries run for HookRetryHorizon, not river's default of about four hours. The wait doubles from HookRetryFirstDelay up to HookRetryMaxDelay.
const ( HookSnoozeDefault = time.Minute HookSnoozeMax = 6 * time.Hour )
A 429 snoozes the job for the receiver's Retry-After, never an attempt: HookSnoozeDefault when the header is absent or unreadable, at most HookSnoozeMax.
const EventHeader = "X-sphericon-Event"
EventHeader carries the event name for receiver-side routing; the signed payload also carries "type". Identity/timestamp/signature use the Standard Webhooks headers (webhook-id, webhook-timestamp, webhook-signature).
const ExcerptBytes = 1 << 10
ExcerptBytes is how much of a receiver's answer a failed call keeps.
Variables ¶
This section is empty.
Functions ¶
func HookMaxAttempts ¶
func HookMaxAttempts() int
HookMaxAttempts is the MaxAttempts to insert a hook job with: the fewest attempts whose last one falls at or after HookRetryHorizon from the first.
func HookNextRetry ¶
HookNextRetry is when to retry after the failed attempt-th attempt, which started at attemptedAt. It is the body of a worker's NextRetry.
func HookRetryDelay ¶
HookRetryDelay is the wait after the attempt-th failed attempt (1-based).
func HookSnooze ¶
HookSnooze is how long to snooze a job whose receiver answered 429 with the given Retry-After value.
func ParseRetryAfter ¶
ParseRetryAfter reads a Retry-After value (RFC 9110: delta-seconds or an HTTP-date) as the wait from now. ok is false for an absent or unreadable value; a date already past is a zero wait.
func Post ¶
Post makes the call through client, which should come from NewClient or NewClientAllowing. It returns a *DeliveryError for a transport failure or a non-2xx answer; the latter carries the first ExcerptBytes of the receiver's response.
func Retryable ¶
Retryable reports whether another attempt can succeed where this failure did not: a timeout, a transport error, a 5xx, a 408 or a 429. A blocked address, and any other status (a 400, a 401, a 404, a 410), would answer the same however long the retries run (ADR 0039).
func Send ¶
func Send(ctx context.Context, client Doer, url, secret, eventName, deliveryID string, body []byte) error
Send signs the body with the endpoint secret (Standard Webhooks scheme) and POSTs it via client. It errors on transport failure or a non-2xx response so the caller can retry. Use a client from NewClient so the SSRF guard applies.
Types ¶
type Allowlist ¶
type Allowlist struct {
// contains filtered or unexported fields
}
Allowlist names the internal host:port pairs an outbound call may reach although the address is private or loopback (ADR 0039). It is an instance-level setting, empty by default and never set in SaaS; there is no per-Workspace form. The zero value allows nothing.
func ParseAllowlist ¶
ParseAllowlist reads a comma- or space-separated list of host:port entries (a hostname, an IPv4 address or a bracketed IPv6 address, then a port). An empty value is the empty allowlist; a malformed entry is an error naming it.
type Call ¶
Call is one outbound POST of a JSON body. A Secret signs it (Standard Webhooks, with ID as the message id); an empty Secret sends it unsigned. Headers are extra request headers, set after the standard ones.
type DeliveryError ¶
type DeliveryError struct {
Kind ErrorKind
Status int
RetryAfter string
// Excerpt is the first ExcerptBytes of the receiver's answer to a KindHTTPStatus failure.
Excerpt string
// contains filtered or unexported fields
}
DeliveryError is a failed delivery with its closed kind. Status is set only for KindHTTPStatus. RetryAfter is the receiver's Retry-After header, verbatim, when it sent one (ParseRetryAfter reads it).
func RateLimited ¶
func RateLimited(sendErr error) *DeliveryError
RateLimited returns the receiver's 429 answer, or nil for any other outcome. A 429 is neither a success nor a failure: the receiver is alive and asking us to slow down.
func (*DeliveryError) Error ¶
func (e *DeliveryError) Error() string
func (*DeliveryError) Unwrap ¶
func (e *DeliveryError) Unwrap() error
type Doer ¶
Doer is the subset of *http.Client used for delivery; both *http.Client and safeurl's wrapped client satisfy it (the latter is not an *http.Client).
func NewClient ¶
NewClient builds an SSRF-hardened HTTP client via doyensec/safeurl: it blocks dialing private/loopback/link-local/CGNAT/metadata addresses on the *resolved* IP (defeating DNS rebinding), restricts schemes to http(s), and does not follow redirects.
func NewClientAllowing ¶
NewClientAllowing is NewClient for the callers that honour the Instance's internal allowlist (ADR 0039): a request to a host:port it names is made without the address check, every other request goes through the hardened client. Redirects are never followed either way, so a permitted host cannot bounce a call to an address that is not.
type ErrorKind ¶
type ErrorKind string
ErrorKind is the closed set of reasons a delivery fails (ADR 0039). It is the vocabulary an owner, the API and an agent read; prose never carries it.
const ( // KindTimeout: the receiver did not answer within the delivery timeout. KindTimeout ErrorKind = "timeout" // KindBlockedAddress: the URL names an address the Instance does not permit // (private, loopback, link-local, or a scheme or port the client refuses). KindBlockedAddress ErrorKind = "blocked_address" // KindHTTPStatus: the receiver answered with a non-2xx status (DeliveryError.Status). KindHTTPStatus ErrorKind = "http_status" // KindNetwork: any other transport failure (DNS, refused or reset connection). KindNetwork ErrorKind = "network" )
type RateLimitVerdict ¶
type RateLimitVerdict struct {
// GiveUp is set for a receiver that never let up in HookRetryHorizon.
GiveUp bool
// Wait is how long to snooze the job otherwise.
Wait time.Duration
}
RateLimitVerdict is what to do with a delivery whose receiver answered 429: end it, or wait. It is a decision, not a queue error: each worker performs its own side effect (an HTTP step records its Enrollment's failure before it gives up; a Webhook delivery touches nothing).
func DecideRateLimited ¶
func DecideRateLimited(sendErr error, createdAt, now time.Time) (RateLimitVerdict, bool)
DecideRateLimited returns the verdict for a delivery that failed with sendErr, and whether it was rate limited at all: a job created at createdAt gives up once HookRetryHorizon has passed, otherwise it waits as long as the receiver asked (HookSnooze).