StatifierRouter.Webhook (StatifierRouter v0.9.2)

Copy Markdown View Source

The webhook front: a Plug-shaped helper that turns one already-verified webhook request into a routing attempt. This package verifies nothing. The host authenticates the request - typically a provider's signature over the raw body - and calls handle/3 only after that verification has passed. A request that reaches this module is one the host has already vouched for.

It is Plug-shaped, not a Plug: it adds no dependency on Plug or Phoenix, and starts no process. The host writes the controller action or the plug and calls handle/3 from it; the README's "A webhook front" section shows the ten lines that takes.

The request

handle/3 takes a plain map the host builds from the connection:

  • :scope - the host's routing scope, a string, as it rides with every event (ADR-0003, section 8);
  • :source - the source the bindings name;
  • :raw_body - the request body exactly as it arrived, the bytes whose signature was verified;
  • :data - the adapter-normalized event, a string-keyed map;
  • :provider_id - the provider's own event id, or nil;
  • :selector - the source's selector, carried for the host's own front and never read here. A binding's selector is the source adapter's to read, and the router never reads it (ADR-0001, section 1); bindings are chosen by source alone.

Any other key is ignored. A request missing :scope, :source, :raw_body or :data, or carrying one of the wrong type, is {:error, {:invalid_request, request}} and nothing is routed.

The message id

The message id is the dedupe key downstream, so which source wins is fixed here rather than left to each host (ADR-0003, section 6):

  • the provider's :provider_id wins whenever it is a non-empty string;
  • otherwise - nil, "", or any non-string - the message id is the lowercase hex SHA-256 of :raw_body.

A provider that sends no event id therefore gets one delivery per distinct body, and its retry of the same body is the same message. There is no third source: :raw_body is required, so an id is always derivable, and the SHA-256 of an empty body is a well-defined, constant id rather than a failure.

A body-hash id is scope-free. The dedupe key is the binding and the message id (StatifierRouter.Dedupe.claim/4), and a binding carries no scope, so the same body under two scopes through one shared binding is one message: the second is a duplicate, and nothing reaches the second scope's execution. A host that routes several scopes through one front therefore either gives each scope its own binding or uses a provider that sends an event id.

The answer and the status

handle/3 answers exactly as StatifierRouter.route/3 does, {:ok, outcomes} or {:error, reason}, and status/1 maps that answer to the HTTP status the provider should see.

Summary

Types

What handle/3 answers, and what status/1 reads.

The request a host hands handle/3 once it has verified the signature. The module documentation describes each key.

Functions

Routes one verified webhook request.

The HTTP status the provider should see for one handle/3 answer.

Types

answer()

@type answer() :: {:ok, [StatifierRouter.outcome()]} | {:error, term()}

What handle/3 answers, and what status/1 reads.

request()

@type request() :: %{
  :scope => String.t(),
  :source => String.t(),
  :raw_body => binary(),
  :data => map(),
  optional(:provider_id) => String.t() | nil,
  optional(:selector) => map(),
  optional(atom()) => term()
}

The request a host hands handle/3 once it has verified the signature. The module documentation describes each key.

Functions

handle(config, request, opts \\ [])

@spec handle(StatifierRouter.Config.t(), request(), keyword()) :: answer()

Routes one verified webhook request.

Derives the message id as the module documentation describes, builds the source event StatifierRouter.route/3 takes and returns that function's answer unchanged: {:ok, outcomes}, one outcome per enabled binding whose source is the request's, or {:error, reason}.

Returns {:error, {:invalid_request, request}}, before any binding is evaluated, for a request this module cannot build an event from.

opts are StatifierRouter.route/3's and are passed through unchanged, so an unknown key is that function's error.

status(arg)

@spec status(answer()) :: 200 | 500

The HTTP status the provider should see for one handle/3 answer.

{:ok, outcomes} is 200, whatever those outcomes are. Every one of them is a recorded answer this router will give again for the same message - a duplicate, a drop, a refusal and a no-match included - so a retry would change nothing and the provider should stop.

{:error, reason} is 500: the attempt did not settle, so the provider should retry.

iex> StatifierRouter.Webhook.status({:ok, [{:duplicate, "clicks_to_join"}]})
200

iex> StatifierRouter.Webhook.status({:error, :no_message_id})
500