Statifier.Send.BasicHTTP (Statifier v2.10.0)

Copy Markdown View Source

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 _ioprocessors carries 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 an ArgumentError naming the option.
  • :transport - a Statifier.Send.BasicHTTP.Transport module the POSTs go through. Default Statifier.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):

  • event becomes the form parameter _scxmleventname, and each namelist entry and <param> a form parameter, in an application/x-www-form-urlencoded body.
  • A <content> child is the body, sent as text/plain; when the send also names an event, _scxmleventname travels as a query parameter of the target URL.
  • The two are told apart by data's shape: a map is form-encoded, :undefined sends _scxmleventname alone 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 target nor targetexpr raises C.2.2's error.communication on 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 same scxml-send-key answers 204 again and enqueues nothing);
  • {:error, {:method_not_allowed, method}} - answer 405 with Allow: POST;
  • any other {:error, _} - answer 400;
  • a location that names no session the front can reach - the front's own 404.

Summary

Types

Why decode/1 could not form an event from a request.

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

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.

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").

Types

decode_error()

@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.

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

cancel(cancel, ctx)

Plans the cancellation of every delayed send this processor holds under cancel.send_id. Pure.

decode(request)

@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 _scxmleventname found, the query string before a form body, else HTTP. and the method in upper case (HTTP.POST).
  • A form body's other parameters, with the query string's, become _event.data, each value through Statifier.EventData's text rung (a predicator literal, else the string), so 2 reads as the number 2. A body of any other content type becomes _event.data through the same text rung, and the query string then contributes the event name only.
  • origintype is the processor URI.
  • :send_key, the scxml-send-key header's value, sets no event field: the front deduplicates on the header's value itself, and the event's sendid stays 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}}.

deliver(send, event, ctx)

Plans one send (see the moduledoc's "Outbound"). Pure.

ioprocessors_entry(type, map)

@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.

perform(arg, ctx)

@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").