The W3C Basic HTTP Event I/O Processor (SCXML appendix C.2), a
Statifier.Send.Processor a host registers like any other send type
(ADR-0069, ADR-0075).
Registering it
Register it under the spec's processor URI and its short form
basichttp, with the base URL the host's own front answers at:
base = "https://example.org/scxml"
Statifier.Session.start_link(chart,
send_types: %{
"http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor" =>
{Statifier.Send.BasicHTTP, base_url: base},
"basichttp" => {Statifier.Send.BasicHTTP, base_url: base}
}
)Neither string is a built-in spelling, so the registration redirects no built-in send (ADR-0075 decision 2). The options:
:base_url(required) - the address the host's front answers at. The session's_ioprocessorscarries an entry under each registered string, both holding the same"location": this URL,/, and the session's_sessionid(C.2.3, ADR-0075 decision 3). A registration without it is refused when the session starts, with anArgumentErrornaming the option.:transport- aStatifier.Send.BasicHTTP.Transportmodule the POSTs go through. DefaultStatifier.Send.BasicHTTP.Transport.Httpc, on OTP's:httpc; this package adds no dependency for it (ADR-0075 decision 6).
Outbound (C.2.2)
deliver/3 plans and perform/2 POSTs, the split every processor has
(Statifier.Send.Processor). The mapping (ADR-0075 decision 4):
eventbecomes the form parameter_scxmleventname, and eachnamelistentry and<param>a form parameter, in anapplication/x-www-form-urlencodedbody.- A
<content>child is the body, sent astext/plain; when the send also names anevent,_scxmleventnametravels as a query parameter of the target URL. - The two are told apart by
data's shape: a map is form-encoded,:undefinedsends_scxmleventnamealone as a form body, and any other value is the body. A<content expr>that evaluates to a map is therefore form-encoded. - A send with neither
targetnortargetexprraises C.2.2'serror.communicationon the sender's internal queue, carrying the send id, and makes no request.
A parameter value is written as text: a string as it is, a number or a
boolean as its literal, nil as null, :undefined as the empty
string. Any other value (a list, a map) is written with inspect/1; its
encoding is not decided yet.
One attempt, and a miss reaches the sender. perform/2 makes one
POST. On a transport error, or a status outside 2xx, it reports the miss
through Statifier.Session.failed_send/3 to the sending session it finds
in Statifier.Registry under the plan context's session_id, so the
sender sees C.1's error.communication carrying the send id, and it
returns {:error, reason}. When no live session is registered under that
id it returns {:error, reason} only, and the dead-letter rule of
failed_send/3's documentation is the host's (ADR-0075 decision 8, point
d).
At-least-once, deduplicated by the receiver (ADR-0075's Amendment of
2026-09-30). The processor keeps no memory across perform/2 calls, so
a host that performs the same instruction twice POSTs twice. Every POST
therefore carries the send's ADR-0054 decision 3 dedup key in the
scxml-send-key header: eight fields joined by /, in the record's
order - the session scope (the plan context's session_id), the send
id, macrostep, microstep, round, c_index, owner and ordinal.
The session scope and the send id are percent-encoded (every byte outside
RFC 3986's unreserved set), the counters are decimal, and owner is
spelled onentry.S.B, onexit.S.B, transition.T or finalize.S.B
with its indexes. A receiver that deduplicates on the header sees each
send once, which is ADR-0069's idempotency MUST end to end; a receiver
that ignores it sees at-least-once delivery.
perform/2 runs in the process that performs the instruction, which for
Statifier.Session is the sending session, so a slow location holds that
session for the length of the request; the default transport bounds each
request with a timeout.
A delayed send is this processor's timer (ADR-0069 decision 4). A
<send delay> is held by a timer process perform/2 starts, and
cancel/2 plans the cancellation of every timer held under the send id
(spec 6.3). The timers are kept in the dictionary of the process that
performs the instructions, so a host that performs them itself performs
a delayed send and its cancel in one process. At fire time the timer
POSTs only if that process is a Statifier.Session still running: a
session that has stopped, or halted, discards the send (spec 6.2).
Inbound (C.2.1)
decode/1 turns one HTTP request into a %Statifier.Event{}. It is pure
and knows no session: a front resolves the location to a session (or to
an execution, for a durable host), calls it, and enqueues the event as
an external event. The status rule a front applies (ADR-0075 decision 5):
{:ok, event}- answer 204 once the event is enqueued, before it is processed (a front that has already enqueued a request carrying the samescxml-send-keyanswers 204 again and enqueues nothing);{:error, {:method_not_allowed, method}}- answer 405 withAllow: POST;- any other
{:error, _}- answer 400; - a location that names no session the front can reach - the front's own 404.
Summary
Functions
Plans the cancellation of every delayed send this processor holds under
cancel.send_id. Pure.
Decodes one HTTP request into an external event (C.2.1, ADR-0075 decision 5). Pure.
Plans one send (see the moduledoc's "Outbound"). Pure.
The _ioprocessors entry for type: a "location" that is the
registration's :base_url, /, and the session's id (C.2.3, ADR-0075
decision 3). Raises ArgumentError when the registration carries no
:base_url, which refuses the session's start.
Types
@type decode_error() :: {:method_not_allowed, String.t()} | {:not_utf8, :query | :body} | {:malformed_send_key, String.t()}
Why decode/1 could not form an event from a request.
@type request() :: %{ :method => String.t(), :content_type => String.t() | nil, :body => binary(), :query => String.t() | nil, optional(:send_key) => String.t() | nil }
What decode/1 is handed: the request's method, its content type (nil
when the request carries none), its body, its query string (nil when
the URL has none), and optionally the value of its scxml-send-key
header (nil or absent when the request carries none).
Functions
@spec cancel( cancel :: Statifier.Effect.Cancel.t(), ctx :: Statifier.Send.Processor.ctx() ) :: {:ok, [Statifier.Send.Processor.instruction()]}
Plans the cancellation of every delayed send this processor holds under
cancel.send_id. Pure.
@spec decode(request :: request()) :: {:ok, Statifier.Event.t()} | {:error, decode_error()}
Decodes one HTTP request into an external event (C.2.1, ADR-0075 decision 5). Pure.
- The method must be POST; any other is
{:error, {:method_not_allowed, method}}. - The event name is the first
_scxmleventnamefound, the query string before a form body, elseHTTP.and the method in upper case (HTTP.POST). - A form body's other parameters, with the query string's, become
_event.data, each value throughStatifier.EventData's text rung (a predicator literal, else the string), so2reads as the number 2. A body of any other content type becomes_event.datathrough the same text rung, and the query string then contributes the event name only. origintypeis the processor URI.:send_key, thescxml-send-keyheader's value, sets no event field: the front deduplicates on the header's value itself, and the event'ssendidstays unset. A value that is not the header's eight fields is{:error, {:malformed_send_key, value}}.
A query string or a body that is not UTF-8 once decoded forms no
datamodel string, and is {:error, {:not_utf8, :query | :body}}.
@spec deliver( send :: Statifier.Effect.Send.t() | Statifier.Effect.SendDelayed.t(), event :: Statifier.Event.t(), ctx :: Statifier.Send.Processor.ctx() ) :: {:ok, [Statifier.Send.Processor.instruction()]}
Plans one send (see the moduledoc's "Outbound"). Pure.
@spec ioprocessors_entry( type :: String.t(), context :: Statifier.Send.Processor.entry_context() ) :: map()
The _ioprocessors entry for type: a "location" that is the
registration's :base_url, /, and the session's id (C.2.3, ADR-0075
decision 3). Raises ArgumentError when the registration carries no
:base_url, which refuses the session's start.
@spec perform(payload :: term(), ctx :: Statifier.Send.Processor.ctx()) :: :ok | {:error, term()}
Performs one instruction deliver/3 or cancel/2 planned: a POST, a
delayed POST's timer, or the cancellation of the timers held under a
send id (see the moduledoc's "Outbound").