Skip to main content

pkg/gateway/webhook/handler.go

Source

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

pkg/gateway/webhook/handler.go#L3
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

pkg/gateway/webhook/handler.go#L19
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

pkg/gateway/webhook/handler.go#L252
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

pkg/gateway/webhook/handler.go#L24
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

pkg/gateway/webhook/handler.go#L86
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

pkg/gateway/webhook/handler.go#L254
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.

pkg/gateway/webhook/handler.go#L39
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 return statement (possibly returning values).
  • 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 return statement (possibly returning values).
  • 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 return statement (possibly returning values).
  • 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 return statement (possibly returning values).
  • 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 return statement (possibly returning values).
  • 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 return statement (possibly returning values).
  • 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.
  • 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.
  • 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.
  • 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 return statement (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.

pkg/gateway/webhook/handler.go#L99
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 return statement (possibly returning values).
  • 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 return statement (possibly returning values).
      • 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 return statement (possibly returning values).
  • 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 return statement (possibly returning values).
  • 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 return statement (possibly returning values).
  • 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.

pkg/gateway/webhook/handler.go#L134
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 return statement (possibly returning values).
  • 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.
  • 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 return statement (possibly returning values).
  • 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 return statement (possibly returning values).
  • 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 return statement (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.

pkg/gateway/webhook/handler.go#L155
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 for loop 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 return statement (possibly returning values).
      • 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 return statement (possibly returning values).
      • 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 select statement 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.
          • 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 return statement (possibly returning values).
  • 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 return statement (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).

pkg/gateway/webhook/handler.go#L188
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 return statement (possibly returning values).
  • 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 return statement (possibly returning values).
  • 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 return statement (possibly returning values).
  • 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 return statement (possibly returning values).
  • 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 return statement (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.

pkg/gateway/webhook/handler.go#L214
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.
  • 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 return statement (possibly returning values).
  • 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 return statement (possibly returning values).
  • 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 return statement (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.

pkg/gateway/webhook/handler.go#L229
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 return statement (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.

pkg/gateway/webhook/handler.go#L235
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 ... range loop.
    • 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 ... range loop.
        • 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.
  • 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.

pkg/gateway/webhook/handler.go#L244
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 return statement (possibly returning values).
  • 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 return statement (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.

pkg/gateway/webhook/handler.go#L259
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 return statement (possibly returning values).

Guides

Reference

Neighboring source