AshDispatch.Workers.SendWebhook (AshDispatch v0.6.12)

View Source

Oban worker for sending webhooks asynchronously.

This worker:

  1. Fetches the DeliveryReceipt by ID
  2. Marks receipt as :sending
  3. Sends HTTP POST to webhook URL
  4. Marks receipt as :sent or :failed

Usage

Jobs are enqueued automatically by webhook transports (Discord, Slack, generic Webhook):

# Discord transport enqueues job
Discord.deliver(receipt, context, channel, event_config)

# Worker processes job asynchronously
%{
  "receipt_id" => "...",
  "webhook_url" => "https://discord.com/api/webhooks/...",
  "payload" => %{
    "content" => "Order #1234 created",
    "embeds" => [...]
  },
  "headers" => %{
    "Content-Type" => "application/json"
  }
}

Retries

Oban handles retries automatically:

  • Max 5 attempts (configurable)
  • Exponential backoff (configurable)
  • Failed jobs can be manually retried

HTTP Client

Uses Req for HTTP requests with:

  • Automatic retries for network errors
  • Timeout handling (10 seconds default)
  • JSON encoding/decoding
  • Comprehensive error reporting

Webhook Formats

Discord and Slack use different JSON formats:

Discord

{
  "content": "Message text",
  "embeds": [{
    "title": "Order Created",
    "description": "Order #1234",
    "color": 5814783
  }]
}

Slack

{
  "text": "Message text",
  "blocks": [{
    "type": "section",
    "text": {"type": "mrkdwn", "text": "Order #1234"}
  }]
}

Summary

Functions

Which Req option carries the request body.

Processes webhook sending job.

Whether a failure is permanent — i.e. retrying sends the identical request and gets the identical answer.

Functions

body_option(args)

@spec body_option(map()) :: [body: binary(), json: term()]

Which Req option carries the request body.

raw_body is an ALREADY serialised body and wins when present: a signed webhook must transmit exactly the bytes that were signed. Letting the HTTP client re-encode a map can change key order or float formatting, and the signature then fails sometimes — which is worse than always, because it looks like a flake instead of a bug.

Without raw_body the behaviour is unchanged (json: payload), so the Discord and Slack transports are unaffected.

perform(job)

Processes webhook sending job.

Job Args

  • receipt_id - DeliveryReceipt UUID
  • webhook_url - Full webhook URL
  • payload - JSON payload to send (encoded by the HTTP client)
  • raw_body - optional pre-serialised body, sent verbatim. Takes precedence over payload; required when the request is signed, so the signed bytes and the sent bytes are the same bytes.
  • headers - Optional HTTP headers (defaults to JSON content type)

Returns

  • :ok on success (2xx response)
  • {:error, reason} on failure (Oban will retry)

permanent?(arg1)

@spec permanent?(term()) :: boolean()

Whether a failure is permanent — i.e. retrying sends the identical request and gets the identical answer.

4xx is the receiver saying this request is wrong, with two exceptions that are explicitly about time rather than content:

  • 408 Request Timeout — the receiver wants it again.
  • 429 Too Many Requests — the receiver wants it later.

Everything else (network errors, timeouts, 5xx) stays retryable: those say not now, not not ever.

A consumer can lean on this: answering 422 when a notification cannot be delivered gives an honest failed receipt on the first attempt instead of five identical attempts and a receipt that only tells the truth minutes later.