pkg/gateway/webhook/handler.go
Source
- Package:
webhook - File:
pkg/gateway/webhook/handler.go - GitHub: https://github.com/theroutercompany/api_router/blob/main/pkg/gateway/webhook/handler.go
Overview
What: Implements webhook ingestion handlers that validate HMAC signatures and forward payloads to a configured target with retries and backoff.
Why: Webhooks provide an event-driven integration mechanism. The gateway needs a secure ingress that can verify authenticity and provide controlled forwarding behavior.
How: The New() constructor validates options and builds a handler. ServeHTTP reads and bounds the request body, verifies the signature, then forwards the payload to the target with retry/backoff rules.
Notes: Webhook secrets should be injected via secret managers. Consider idempotency on the target side because retries may deliver duplicates.
Contents
Imports
import block 1
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
pkglog "github.com/theroutercompany/api_router/pkg/log"
)
Constants
const block 1
const (
defaultMaxBodyBytes int64 = 1 << 20 // 1 MiB
)
defaultMaxBodyBytes
What: Default maximum request body size (1 MiB) for webhook payloads.
Why: Prevents large payloads from exhausting memory and creating unbounded forwarding work.
How: Used when Options.MaxBodyBytes is unset or invalid.
Variables
var block 1
var errUpstreamRetryable = errors.New("retryable upstream failure")
errUpstreamRetryable
What: Sentinel error used to mark failures that should be retried.
Why: Allows retry logic to distinguish between retryable and non-retryable errors using errors.Is.
How: Wrapped with %w by forwardOnce for network errors and non-4xx status codes.
Types
type block 1
type Options struct {
Name string
Path string
TargetURL string
Secret string
SignatureHeader string
MaxAttempts int
InitialBackoff time.Duration
Timeout time.Duration
Client *http.Client
Logger pkglog.Logger
MaxBodyBytes int64
}
Options
What: Configuration for the webhook handler (target, secret, signature header, retry/backoff/timeouts, client/logger, body size).
Why: Makes webhook handler behavior explicit and configurable from server/config packages.
How: Passed to New() and converted into a private webhookHandler.
type block 2
type webhookHandler struct {
name string
targetURL string
secret []byte
signatureHeader string
maxAttempts int
initialBackoff time.Duration
timeout time.Duration
client *http.Client
logger pkglog.Logger
maxBodyBytes int64
}
webhookHandler
What: Internal handler implementation that holds immutable webhook configuration.
Why: Keeps the public API small while allowing server/config to construct many handlers with different settings.
How: Stores secret bytes, target URL, retry parameters, client, logger, and body limits used by ServeHTTP and forwarding helpers.
type block 3
type errBodyTooLarge struct {
size int64
limit int64
}
errBodyTooLarge
What: Error type returned when the request body exceeds the configured size limit.
Why: Allows ServeHTTP to detect oversize payloads and map them to 413 responses.
How: Returned by readRequestBody when more than maxBytes are read.
Functions and Methods
New
What: Constructs a webhook http.Handler with signature verification and forwarding settings.
Why: Validates required inputs and provides safe defaults for client, logger, and body size limits.
How: Checks required options (secret, target URL, signature header, retry/backoff/timeouts), sets defaults for optional fields, and returns a configured webhookHandler.
func New(opts Options) (http.Handler, error) {
if strings.TrimSpace(opts.Secret) == "" {
return nil, errors.New("webhook secret required")
}
if strings.TrimSpace(opts.TargetURL) == "" {
return nil, errors.New("webhook targetURL required")
}
if strings.TrimSpace(opts.SignatureHeader) == "" {
return nil, errors.New("webhook signature header required")
}
if opts.MaxAttempts <= 0 {
return nil, errors.New("webhook maxAttempts must be positive")
}
if opts.InitialBackoff <= 0 {
return nil, errors.New("webhook initial backoff must be positive")
}
if opts.Timeout <= 0 {
return nil, errors.New("webhook timeout must be positive")
}
if opts.Client == nil {
opts.Client = &http.Client{
Timeout: opts.Timeout,
}
}
if opts.Logger == nil {
opts.Logger = pkglog.Shared()
}
if opts.MaxBodyBytes <= 0 {
opts.MaxBodyBytes = defaultMaxBodyBytes
}
handler := &webhookHandler{
name: opts.Name,
targetURL: opts.TargetURL,
secret: []byte(opts.Secret),
signatureHeader: opts.SignatureHeader,
maxAttempts: opts.MaxAttempts,
initialBackoff: opts.InitialBackoff,
timeout: opts.Timeout,
client: opts.Client,
logger: opts.Logger,
maxBodyBytes: opts.MaxBodyBytes,
}
return handler, nil
}
Walkthrough
The list below documents the statements inside the function body, including nested blocks and inline closures.
- L40:
if strings.TrimSpace(opts.Secret) == "" { return nil, errors.New("webhook secret required") }- What: Branches conditionally.
- Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L41:
return nil, errors.New("webhook secret required")- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
- L41:
- L43:
if strings.TrimSpace(opts.TargetURL) == "" { return nil, errors.New("webhook targetURL required") }- What: Branches conditionally.
- Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L44:
return nil, errors.New("webhook targetURL required")- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
- L44:
- L46:
if strings.TrimSpace(opts.SignatureHeader) == "" { return nil, errors.New("webhook signature header required") }- What: Branches conditionally.
- Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L47:
return nil, errors.New("webhook signature header required")- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
- L47:
- L49:
if opts.MaxAttempts <= 0 { return nil, errors.New("webhook maxAttempts must be positive") }- What: Branches conditionally.
- Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L50:
return nil, errors.New("webhook maxAttempts must be positive")- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
- L50:
- L52:
if opts.InitialBackoff <= 0 { return nil, errors.New("webhook initial backoff must be positive") }- What: Branches conditionally.
- Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L53:
return nil, errors.New("webhook initial backoff must be positive")- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
- L53:
- L55:
if opts.Timeout <= 0 { return nil, errors.New("webhook timeout must be positive") }- What: Branches conditionally.
- Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L56:
return nil, errors.New("webhook timeout must be positive")- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
- L56:
- L58:
if opts.Client == nil { opts.Client = &http.Client{ Timeout: opts.Timeout, } }- What: Branches conditionally.
- Why: Handles different execution paths based on runtime state.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L59:
opts.Client = &http.Client{ Timeout: opts.Timeout, }- What: Assigns opts.Client.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L59:
- L63:
if opts.Logger == nil { opts.Logger = pkglog.Shared() }- What: Branches conditionally.
- Why: Handles different execution paths based on runtime state.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L64:
opts.Logger = pkglog.Shared()- What: Assigns opts.Logger.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L64:
- L66:
if opts.MaxBodyBytes <= 0 { opts.MaxBodyBytes = defaultMaxBodyBytes }- What: Branches conditionally.
- Why: Handles different execution paths based on runtime state.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L67:
opts.MaxBodyBytes = defaultMaxBodyBytes- What: Assigns opts.MaxBodyBytes.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L67:
- L70:
handler := &webhookHandler{ name: opts.Name, targetURL: opts.TargetURL, secret: []byte(opts.Secret), signatureHeader: opts.SignatureHeader,…- What: Defines handler.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L83:
return handler, nil- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
(*webhookHandler).ServeHTTP
What: Main HTTP handler for webhook ingestion.
Why: Orchestrates the end-to-end flow of method validation, body read, signature check, and forwarding.
How: Requires POST, reads and bounds the body, verifies signature, forwards with retry/backoff, and returns 202 Accepted on success.
func (h *webhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.Header().Set("Allow", http.MethodPost)
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
defer r.Body.Close()
body, err := readRequestBody(r.Body, h.maxBodyBytes)
if err != nil {
var tooLarge *errBodyTooLarge
if errors.As(err, &tooLarge) {
http.Error(w, "payload too large", http.StatusRequestEntityTooLarge)
return
}
h.logger.Errorw("webhook read body failed", "error", err, "webhook", h.name)
http.Error(w, "invalid payload", http.StatusBadRequest)
return
}
if err := h.verifySignature(r.Header.Get(h.signatureHeader), body); err != nil {
h.logger.Warnw("webhook signature verification failed", "error", err, "webhook", h.name)
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
if err := h.forwardWithRetry(r.Context(), r, body); err != nil {
h.logger.Errorw("webhook delivery failed", "error", err, "webhook", h.name)
http.Error(w, "upstream unavailable", http.StatusBadGateway)
return
}
w.WriteHeader(http.StatusAccepted)
}
Walkthrough
The list below documents the statements inside the function body, including nested blocks and inline closures.
- L100:
if r.Method != http.MethodPost { w.Header().Set("Allow", http.MethodPost) http.Error(w, "method not allowed", http.StatusMethodNotAllowed) …- What: Branches conditionally.
- Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L101:
w.Header().Set("Allow", http.MethodPost)- What: Calls w.Header().Set.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
- L102:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)- What: Calls http.Error.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
- L103:
return- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
- L101:
- L105:
defer r.Body.Close()- What: Defers a call for cleanup.
- Why: Ensures the deferred action runs even on early returns.
- How: Schedules the call to run when the surrounding function returns.
- L107:
body, err := readRequestBody(r.Body, h.maxBodyBytes)- What: Defines body, err.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L108:
if err != nil { var tooLarge *errBodyTooLarge if errors.As(err, &tooLarge) { http.Error(w, "payload too large", http.StatusRequestEntityToo…- What: Branches conditionally.
- Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L109:
var tooLarge *errBodyTooLarge- What: Declares local names.
- Why: Introduces variables or types used later in the function.
- How: Executes a Go declaration statement inside the function body.
- L110:
if errors.As(err, &tooLarge) { http.Error(w, "payload too large", http.StatusRequestEntityTooLarge) return }- What: Branches conditionally.
- Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L111:
http.Error(w, "payload too large", http.StatusRequestEntityTooLarge)- What: Calls http.Error.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
- L112:
return- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
- L111:
- L114:
h.logger.Errorw("webhook read body failed", "error", err, "webhook", h.name)- What: Calls h.logger.Errorw.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
- L115:
http.Error(w, "invalid payload", http.StatusBadRequest)- What: Calls http.Error.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
- L116:
return- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
- L109:
- L119:
if err := h.verifySignature(r.Header.Get(h.signatureHeader), body); err != nil { h.logger.Warnw("webhook signature verification failed", "e…- What: Branches conditionally.
- Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L119:
err := h.verifySignature(r.Header.Get(h.signatureHeader), body)- What: Defines err.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L120:
h.logger.Warnw("webhook signature verification failed", "error", err, "webhook", h.name)- What: Calls h.logger.Warnw.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
- L121:
http.Error(w, "invalid signature", http.StatusUnauthorized)- What: Calls http.Error.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
- L122:
return- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
- L119:
- L125:
if err := h.forwardWithRetry(r.Context(), r, body); err != nil { h.logger.Errorw("webhook delivery failed", "error", err, "webhook", h.name…- What: Branches conditionally.
- Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L125:
err := h.forwardWithRetry(r.Context(), r, body)- What: Defines err.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L126:
h.logger.Errorw("webhook delivery failed", "error", err, "webhook", h.name)- What: Calls h.logger.Errorw.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
- L127:
http.Error(w, "upstream unavailable", http.StatusBadGateway)- What: Calls http.Error.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
- L128:
return- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
- L125:
- L131:
w.WriteHeader(http.StatusAccepted)- What: Calls w.WriteHeader.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
(*webhookHandler).verifySignature
What: Verifies the incoming webhook signature header against the request body.
Why: Prevents unauthenticated callers from injecting arbitrary webhook events.
How: Trims the header, strips an optional sha256= prefix, hex-decodes the signature, computes the expected HMAC, and compares using hmac.Equal.
func (h *webhookHandler) verifySignature(sigHeader string, body []byte) error {
sig := strings.TrimSpace(sigHeader)
if sig == "" {
return errors.New("signature header missing")
}
if strings.HasPrefix(strings.ToLower(sig), "sha256=") {
sig = sig[7:]
}
expectedMAC := computeHMAC(body, h.secret)
provided, err := hex.DecodeString(sig)
if err != nil {
return fmt.Errorf("invalid signature encoding: %w", err)
}
if !hmac.Equal(expectedMAC, provided) {
return errors.New("signature mismatch")
}
return nil
}
Walkthrough
The list below documents the statements inside the function body, including nested blocks and inline closures.
- L135:
sig := strings.TrimSpace(sigHeader)- What: Defines sig.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L136:
if sig == "" { return errors.New("signature header missing") }- What: Branches conditionally.
- Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L137:
return errors.New("signature header missing")- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
- L137:
- L139:
if strings.HasPrefix(strings.ToLower(sig), "sha256=") { sig = sig[7:] }- What: Branches conditionally.
- Why: Handles different execution paths based on runtime state.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L140:
sig = sig[7:]- What: Assigns sig.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L140:
- L143:
expectedMAC := computeHMAC(body, h.secret)- What: Defines expectedMAC.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L144:
provided, err := hex.DecodeString(sig)- What: Defines provided, err.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L145:
if err != nil { return fmt.Errorf("invalid signature encoding: %w", err) }- What: Branches conditionally.
- Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L146:
return fmt.Errorf("invalid signature encoding: %w", err)- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
- L146:
- L149:
if !hmac.Equal(expectedMAC, provided) { return errors.New("signature mismatch") }- What: Branches conditionally.
- Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L150:
return errors.New("signature mismatch")- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
- L150:
- L152:
return nil- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
(*webhookHandler).forwardWithRetry
What: Forwards the webhook payload with retry and backoff on retryable failures.
Why: Webhook targets can experience transient failures; retries improve delivery reliability.
How: Runs up to maxAttempts, each with its own timeout context; retries only when errors are wrapped with errUpstreamRetryable, sleeping with exponential backoff between attempts.
func (h *webhookHandler) forwardWithRetry(parentCtx context.Context, original *http.Request, body []byte) error {
backoff := h.initialBackoff
for attempt := 1; attempt <= h.maxAttempts; attempt++ {
ctx, cancel := context.WithTimeout(parentCtx, h.timeout)
err := h.forwardOnce(ctx, original, body, attempt)
cancel()
if err == nil {
return nil
}
retryable := errors.Is(err, errUpstreamRetryable)
h.logger.Warnw("webhook delivery attempt failed",
"webhook", h.name,
"attempt", attempt,
"maxAttempts", h.maxAttempts,
"retryable", retryable,
"error", err,
)
if !retryable || attempt == h.maxAttempts {
return err
}
select {
case <-time.After(backoff):
backoff = increaseBackoff(backoff, 4*time.Second)
case <-parentCtx.Done():
return parentCtx.Err()
}
}
return errors.New("webhook delivery exhausted retries")
}
Walkthrough
The list below documents the statements inside the function body, including nested blocks and inline closures.
- L156:
backoff := h.initialBackoff- What: Defines backoff.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L157:
for attempt := 1; attempt <= h.maxAttempts; attempt++ { ctx, cancel := context.WithTimeout(parentCtx, h.timeout) err := h.forwardOnce(ctx, …- What: Runs a loop.
- Why: Repeats logic until a condition is met or the loop terminates.
- How: Executes a
forloop statement. - Nested steps:
- L157:
attempt := 1- What: Defines attempt.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L157:
attempt++- What: Updates a counter.
- Why: Maintains an index or tally used by subsequent logic.
- How: Executes an increment/decrement statement.
- L158:
ctx, cancel := context.WithTimeout(parentCtx, h.timeout)- What: Defines ctx, cancel.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L159:
err := h.forwardOnce(ctx, original, body, attempt)- What: Defines err.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L160:
cancel()- What: Calls cancel.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
- L161:
if err == nil { return nil }- What: Branches conditionally.
- Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L162:
return nil- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
- L162:
- L165:
retryable := errors.Is(err, errUpstreamRetryable)- What: Defines retryable.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L166:
h.logger.Warnw("webhook delivery attempt failed", "webhook", h.name, "attempt", attempt, "maxAttempts", h.maxAttempts, "retryable", retryab…- What: Calls h.logger.Warnw.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
- L174:
if !retryable || attempt == h.maxAttempts { return err }- What: Branches conditionally.
- Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L175:
return err- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
- L175:
- L178:
select { case <-time.After(backoff): backoff = increaseBackoff(backoff, 4*time.Second) case <-parentCtx.Done(): return parentCtx.Err() }- What: Selects among concurrent operations.
- Why: Coordinates channel operations without blocking incorrectly.
- How: Executes a
selectstatement and runs one ready case. - Nested steps:
- L179:
case <-time.After(backoff):- What: Selects a select-case branch.
- Why: Coordinates concurrent operations without blocking incorrectly.
- How: Runs this case body when its channel operation is ready (or runs default immediately).
- Nested steps:
- L180:
backoff = increaseBackoff(backoff, 4*time.Second)- What: Assigns backoff.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L180:
- L181:
case <-parentCtx.Done():- What: Selects a select-case branch.
- Why: Coordinates concurrent operations without blocking incorrectly.
- How: Runs this case body when its channel operation is ready (or runs default immediately).
- Nested steps:
- L182:
return parentCtx.Err()- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
- L182:
- L179:
- L157:
- L185:
return errors.New("webhook delivery exhausted retries")- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
(*webhookHandler).forwardOnce
What: Performs a single forwarding attempt to the configured target.
Why: Separates one attempt from retry logic and keeps retry policy centralized.
How: Creates a POST request with the same body, copies headers, adds X-Router-Webhook-* headers, executes the request, and classifies status codes as success, non-retryable (4xx), or retryable (5xx/network).
func (h *webhookHandler) forwardOnce(ctx context.Context, original *http.Request, body []byte, attempt int) error {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, h.targetURL, bytes.NewReader(body))
if err != nil {
return fmt.Errorf("build request: %w", err)
}
copyHeaders(original.Header, req.Header)
req.Header.Set("X-Router-Webhook-Name", h.name)
req.Header.Set("X-Router-Webhook-Attempt", fmt.Sprintf("%d", attempt))
resp, err := h.client.Do(req)
if err != nil {
return fmt.Errorf("%w: %v", errUpstreamRetryable, err)
}
defer resp.Body.Close()
io.Copy(io.Discard, resp.Body)
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return nil
}
if resp.StatusCode >= 400 && resp.StatusCode < 500 {
return fmt.Errorf("upstream returned status %d", resp.StatusCode)
}
return fmt.Errorf("%w: upstream status %d", errUpstreamRetryable, resp.StatusCode)
}
Walkthrough
The list below documents the statements inside the function body, including nested blocks and inline closures.
- L189:
req, err := http.NewRequestWithContext(ctx, http.MethodPost, h.targetURL, bytes.NewReader(body))- What: Defines req, err.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L190:
if err != nil { return fmt.Errorf("build request: %w", err) }- What: Branches conditionally.
- Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L191:
return fmt.Errorf("build request: %w", err)- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
- L191:
- L194:
copyHeaders(original.Header, req.Header)- What: Calls copyHeaders.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
- L195:
req.Header.Set("X-Router-Webhook-Name", h.name)- What: Calls req.Header.Set.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
- L196:
req.Header.Set("X-Router-Webhook-Attempt", fmt.Sprintf("%d", attempt))- What: Calls req.Header.Set.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
- L198:
resp, err := h.client.Do(req)- What: Defines resp, err.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L199:
if err != nil { return fmt.Errorf("%w: %v", errUpstreamRetryable, err) }- What: Branches conditionally.
- Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L200:
return fmt.Errorf("%w: %v", errUpstreamRetryable, err)- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
- L200:
- L202:
defer resp.Body.Close()- What: Defers a call for cleanup.
- Why: Ensures the deferred action runs even on early returns.
- How: Schedules the call to run when the surrounding function returns.
- L203:
io.Copy(io.Discard, resp.Body)- What: Calls io.Copy.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
- L205:
if resp.StatusCode >= 200 && resp.StatusCode < 300 { return nil }- What: Branches conditionally.
- Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L206:
return nil- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
- L206:
- L208:
if resp.StatusCode >= 400 && resp.StatusCode < 500 { return fmt.Errorf("upstream returned status %d", resp.StatusCode) }- What: Branches conditionally.
- Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L209:
return fmt.Errorf("upstream returned status %d", resp.StatusCode)- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
- L209:
- L211:
return fmt.Errorf("%w: upstream status %d", errUpstreamRetryable, resp.StatusCode)- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
readRequestBody
What: Reads the request body up to a maximum number of bytes.
Why: Webhooks must be bounded in size for safety and predictability.
How: Uses io.LimitReader to read at most maxBytes+1, returning errBodyTooLarge when the limit is exceeded.
func readRequestBody(body io.Reader, maxBytes int64) ([]byte, error) {
if maxBytes <= 0 {
maxBytes = defaultMaxBodyBytes
}
limited := io.LimitReader(body, maxBytes+1)
data, err := io.ReadAll(limited)
if err != nil {
return nil, err
}
if int64(len(data)) > maxBytes {
return nil, &errBodyTooLarge{size: int64(len(data)), limit: maxBytes}
}
return data, nil
}
Walkthrough
The list below documents the statements inside the function body, including nested blocks and inline closures.
- L215:
if maxBytes <= 0 { maxBytes = defaultMaxBodyBytes }- What: Branches conditionally.
- Why: Handles different execution paths based on runtime state.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L216:
maxBytes = defaultMaxBodyBytes- What: Assigns maxBytes.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L216:
- L218:
limited := io.LimitReader(body, maxBytes+1)- What: Defines limited.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L219:
data, err := io.ReadAll(limited)- What: Defines data, err.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L220:
if err != nil { return nil, err }- What: Branches conditionally.
- Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L221:
return nil, err- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
- L221:
- L223:
if int64(len(data)) > maxBytes { return nil, &errBodyTooLarge{size: int64(len(data)), limit: maxBytes} }- What: Branches conditionally.
- Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L224:
return nil, &errBodyTooLarge{size: int64(len(data)), limit: maxBytes}- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
- L224:
- L226:
return data, nil- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
computeHMAC
What: Computes an HMAC-SHA256 digest for a request body using a secret key.
Why: HMAC validation is the authenticity mechanism for incoming webhooks.
How: Uses crypto/hmac with sha256.New and returns the raw digest bytes.
func computeHMAC(body []byte, secret []byte) []byte {
mac := hmac.New(sha256.New, secret)
mac.Write(body)
return mac.Sum(nil)
}
Walkthrough
The list below documents the statements inside the function body, including nested blocks and inline closures.
- L230:
mac := hmac.New(sha256.New, secret)- What: Defines mac.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L231:
mac.Write(body)- What: Calls mac.Write.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
- L232:
return mac.Sum(nil)- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
copyHeaders
What: Copies HTTP headers from the incoming webhook request to the forwarded request.
Why: Some webhook senders include metadata headers that the target may need.
How: Adds all header values and deletes Host to avoid forcing a host override on the outgoing request.
func copyHeaders(src http.Header, dst http.Header) {
for key, values := range src {
for _, value := range values {
dst.Add(key, value)
}
}
dst.Del("Host")
}
Walkthrough
The list below documents the statements inside the function body, including nested blocks and inline closures.
- L236:
for key, values := range src { for _, value := range values { dst.Add(key, value) } }- What: Iterates over a collection.
- Why: Processes multiple elements with the same logic.
- How: Executes a
for ... rangeloop. - Nested steps:
- L237:
for _, value := range values { dst.Add(key, value) }- What: Iterates over a collection.
- Why: Processes multiple elements with the same logic.
- How: Executes a
for ... rangeloop. - Nested steps:
- L238:
dst.Add(key, value)- What: Calls dst.Add.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
- L238:
- L237:
- L241:
dst.Del("Host")- What: Calls dst.Del.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
increaseBackoff
What: Computes the next backoff duration for a retry loop with an upper bound.
Why: Exponential backoff reduces load on failing upstreams and avoids tight retry loops.
How: Doubles the current duration and clamps to the provided max duration.
func increaseBackoff(current time.Duration, max time.Duration) time.Duration {
next := current * 2
if next > max {
return max
}
return next
}
Walkthrough
The list below documents the statements inside the function body, including nested blocks and inline closures.
- L245:
next := current * 2- What: Defines next.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L246:
if next > max { return max }- What: Branches conditionally.
- Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L247:
return max- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
- L247:
- L249:
return next- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
(*errBodyTooLarge).Error
What: Formats an error message describing how much the request exceeded the body size limit.
Why: Useful for debugging and logging size-related rejections.
How: Returns a string with the actual size and configured limit.
func (e *errBodyTooLarge) Error() string {
return fmt.Sprintf("body size %d exceeds limit %d bytes", e.size, e.limit)
}
Walkthrough
The list below documents the statements inside the function body, including nested blocks and inline closures.
- L260:
return fmt.Sprintf("body size %d exceeds limit %d bytes", e.size, e.limit)- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).