StatifierRouter.BasicHTTP.Front (StatifierRouter v0.9.2)

Copy Markdown View Source

The BasicHTTP front for durable executions: a Plug-shaped helper that takes one POST at an execution's location, decodes it with Statifier.Send.BasicHTTP.decode/1, and delivers the event to the execution the location names (ADR-0002, the Amendment of 2026-09-30, decisions 3 and 5).

A location is a bearer capability. Anyone who holds it can post events to that execution, and this front authenticates nothing beyond possession of the location (ruled by the operator, 2026-09-30). Hand a location only to the parties that should reach the execution, keep it out of logs, serve the base URL over TLS, and rotate it with StatifierRouter.BasicHTTP.rotate_location/2 when it may have leaked.

It is Plug-shaped, not a Plug, as StatifierRouter.Webhook is: it adds no dependency on Plug or Phoenix and starts no process. The host routes POST <base_url>/:token (and, for the 405, every other method) to a controller action that builds the request map and calls handle/3, then answers with response/1. The README's "A BasicHTTP front" section shows it. handle/3 needs a configuration that sets :basichttp.

The request

  • :token - the path segment after the base URL, which the host cuts from the request path;
  • :method - the request's method, as a string;
  • :content_type - its content-type header, or nil;
  • :body - its body, as it arrived;
  • :query - its query string, or nil;
  • :send_key - its scxml-send-key header, or nil; optional.

The last five are handed to Statifier.Send.BasicHTTP.decode/1 as they are. A request missing one of the first five, or carrying any of them with the wrong type, is {:error, {:invalid_request, keys}}, naming the keys and never the token.

What one request does

The request is checked, then the token is resolved, then the request is decoded, then the event is delivered. While a route runs in the calling process the front refuses with {:error, {:reentrant_route, execution_id}} before it resolves anything, as StatifierRouter.Delivery.deliver/4 does.

  • Resolution. A token with no location row, a string that is not a minted token's shape, and an address row already stamped terminal_seen_at are each {:error, :unknown_location}: the front delivers nothing and writes nothing. The answer does not say which.
  • Delivery. The event goes through StatifierRouter.Delivery.deliver_event/4 under the plan name basichttp, the address row's document and key, create: :never and ADR-0001's default horizon, with the row's scope. That is the one transaction every delivery takes: the dedupe claim, the address lookup, the step, the ledger row, whose binding_id is basichttp.
  • Deduplication. With a send key, the claim's message id is the execution id, /, and the key, so one key is deduplicated per execution; a request already enqueued within the horizon is a duplicate and nothing is enqueued. Without one, the message id is minted fresh and every such request is delivered. The key is the sender's claim, trusted as far as the location is.

A request that fails before the delivery writes no row: before the token resolves there is no scope, and the ledger's scope is NOT NULL. No row and no error the front returns carries the token.

The answer and the status

handle/3 answers {:ok, outcome}, one of StatifierRouter.Delivery.deliver_event/4's outcomes, or {:error, reason}. response/1 maps it to the status and headers to answer with:

handle/3 answersstatusheaders
{:ok, {:delivered, "basichttp", execution_id}}204none
{:ok, {:duplicate, "basichttp"}}204none
{:ok, {:dropped, "basichttp", :finished}}404none
{:ok, {:dropped, "basichttp", :no_execution}}404none
{:error, :unknown_location}404none
{:error, {:method_not_allowed, method}}405allow: POST
any other decode error400none
any other {:error, reason}500none

An event the execution selects no transition for is delivered and answered 204: it was added to the execution's queue. A dropped: finished stamps the address row terminal, so a retry of the same POST resolves as an unknown location and is answered 404 again. A 500 is a delivery that did not settle, and a sender may retry it.

Summary

Types

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

The request a host hands handle/3; the moduledoc describes each key.

Functions

Takes one request at a location, as the module documentation describes.

The status and headers to answer one handle/3 answer with; the module documentation's table lists each.

Types

answer()

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

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

request()

@type request() :: %{
  :token => String.t(),
  :method => String.t(),
  :content_type => String.t() | nil,
  :body => binary(),
  :query => String.t() | nil,
  optional(:send_key) => String.t() | nil,
  optional(atom()) => term()
}

The request a host hands handle/3; the moduledoc describes each key.

Functions

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

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

Takes one request at a location, as the module documentation describes.

opts: :now, a DateTime in UTC, the time the delivery's rows carry, defaulting to DateTime.utc_now/0. Any other key is {:error, {:unknown_key, name}}.

response(arg1)

@spec response(answer()) :: {100..599, [{String.t(), String.t()}]}

The status and headers to answer one handle/3 answer with; the module documentation's table lists each.

iex> StatifierRouter.BasicHTTP.Front.response({:ok, {:duplicate, "basichttp"}})
{204, []}

iex> StatifierRouter.BasicHTTP.Front.response({:error, {:method_not_allowed, "GET"}})
{405, [{"allow", "POST"}]}