# `Statifier.Send.BasicHTTP`
[🔗](https://github.com/riddler/statifier-ex/blob/v2.10.0/lib/statifier/send/basic_http.ex#L1)

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.

# `decode_error`

```elixir
@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`

```elixir
@type request() :: %{
  :method =&gt; String.t(),
  :content_type =&gt; String.t() | nil,
  :body =&gt; binary(),
  :query =&gt; String.t() | nil,
  optional(:send_key) =&gt; 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).

# `cancel`

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

# `decode`

```elixir
@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`

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

# `ioprocessors_entry`

```elixir
@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`

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
