Skip to content

Adds the Basic HTTP Event I/O Processor - #353

Merged
johnnyt merged 3 commits into
mainfrom
st-gje8-basichttp-processor
Sep 30, 2026
Merged

johnnyt merged 3 commits into
mainfrom
st-gje8-basichttp-processor

Conversation

@johnnyt

@johnnyt johnnyt commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Why

ADR-0075 (at proposed on main) decides how this package supports the W3C Basic HTTP Event I/O Processor (SCXML appendix C.2): a registered send type shipped here, a pure inbound decoder, an injected transport with an OTP :httpc default, and the contract changes the processor needs. This PR is the engine half the record assigns to this repository: decisions 1-6, decision 8 points a, b, c, d and f, and the decision 9 rows for the processor. It also carries an Amendment to ADR-0075, at proposed, appended at the end of the record (zero removed lines): every POST carries the send's dedup key, and the receiver deduplicates. The corpus half (decision 7: the schema key, the corpus host, the transform, the exclusions and the claims) is a separate change.

What

  • Statifier.Send.BasicHTTP: a Statifier.Send.Processor. deliver/3 maps event, namelist, <param> and <content> onto a POST as decision 4 says (the event name in the query string when the body is content), and plans C.2.2's error.communication for a send with no target. perform/2 makes one attempt through the transport and reports a transport error or a non-2xx status through Statifier.Session.failed_send/3 (decision 8, point d); a delayed send is the processor's timer and a <cancel> stops it (decision 9). decode/1 is the pure inbound decoder of decision 5 (the first _scxmleventname, query before body, else HTTP.POST; form values and other bodies through the text rung). ioprocessors_entry/2 writes the location, the base URL, / and the session id, and refuses a registration without :base_url when the session starts.
  • The dedup key (ADR-0075's Amendment of 2026-09-30; the header, at-least-once delivery and receiver deduplication ruled by the operator, 2026-09-30, and the decoder's handling of the header the record's): every POST, immediate or delayed, form or content body, carries the send's ADR-0054 decision 3 dedup key in an scxml-send-key header, its eight components joined by / (session scope and send id percent-encoded, counters decimal, owner spelled onentry.S.B, onexit.S.B, finalize.S.B or transition.T). The processor is at-least-once; a receiver that deduplicates on the header delivers each send once, which is ADR-0069's idempotency MUST end to end, and one that ignores it sees at-least-once delivery. decode/1 takes the header's value as :send_key, sets no event field from it (an inbound event's sendid stays unset) and refuses a malformed one (400); a front deduplicates on the header's value itself, and the loopback front hands the header over and does not deduplicate.
  • Statifier.Send.BasicHTTP.Transport (one callback) and Statifier.Send.BasicHTTP.Transport.Httpc, always compiled, TLS verified against the system CA store, bounded by timeouts, starting :inets and :ssl itself on first use and checking that :ssl and :public_key can be loaded, answering {:error, reason} when they cannot. mix.exs, mix.lock and the application list are unchanged.
  • The {module, opts} :send_types value (decision 8, point b): Statifier.Send.Types keeps each type's module and options; Statifier.Evaluator.SystemVariables.initial/3 asks the new optional ioprocessors_entry/2 callback with the session id and the options (decision 3; a module exporting only /1 is asked as before); the planner's lookup adds :opts to the context of deliver/3 and cancel/2 for a {module, opts} registration only; Statifier.Session.Recording writes the options as strings.
  • Statifier.Testing.Case.test_scxml/5 takes a :send_types option; a call without it starts its session as before.
  • Mix.Statifier.BasicHTTPFront: the loopback front on :inets httpd for this repository's own runs, answering by the status rule (204, 405 with Allow: POST, 400, 404).
  • The two stale sentences in lib/ the record's Consequences name (Statifier.Send.Target.supported_type?/1's doc, Statifier.Effect.Send's moduledoc) now name the processor.

A session that registers nothing sees no change: send_types stays nil, _ioprocessors holds the SCXML entry alone, and a bare-module registration's plan context gains no key (pinned by a test).

Provenance

Engineering choices inside the record's scope, each reversible before a release:

  • The front lives in lib/mix/statifier/ (repository tooling, not packaged) rather than test/support/, so the corpus host can start it outside the test environment; the record lists it among the changes in lib/.
  • perform/2 still receives the plan context without :opts (the session performs a {:handler, module, payload} instruction knowing only the module), so deliver/3 carries the transport in the payload it plans.
  • The delayed send's timer is an unsupervised process started by perform/2, monitoring the performing process and kept in that process's dictionary under the send id; at fire time it POSTs only while that process is a running session (spec 6.2's discard). Each effect call on those lines cites ADR-0075 for the effects rule.
  • The decoder refuses a query string or body that is not UTF-8 once decoded ({:not_utf8, part}, answered 400); a non-form body takes the query string's event name only.
  • The :httpc and :public_key calls are kept to two functions carrying @dialyzer {:nowarn_function, ...} (and @compile {:no_warn_undefined, [:public_key]}), because those OTP applications are deliberately not in the application list and so not in dialyzer's PLT. The front does the same for its two httpd functions.
  • Cure after the first review pass: the blocking finding (the processor did not meet ADR-0069's idempotency MUST) is answered by the Amendment and the header above, as ruled by the operator, 2026-09-30. The same cure makes the :httpc adapter answer {:error, {:not_loadable, module, reason}} when :ssl or :public_key cannot be loaded; before it, a POST under a Mix task whose code path had been pruned crashed inside httpc even for an http: URL (reproduced with mix run in the dev environment; the same run now answers the error).
  • Cure after the second review pass: the first cure had decode/1 set the inbound event's sendid from the key's send id, which the ruling did not decide and which a generated send id does not justify under 5.10.1. That rule is gone from the Amendment's decoder paragraph (edited in place; the Amendment has never been on main, and the record diff still removes no line) and from the code, and a test pins that a POST whose key names a generated send id delivers an event with no sendid.
  • A recorded {module, opts} writes each option key as a string and each atom value as {:atom, name}; unresolvable names join {:unknown_handler_modules, names}.

Notes

  • Gate: the full mix quality is green on the head (Tests 3,121 of 3,121, 96.3% coverage; Dialyzer, Doctor, Credo, ADR guard all green), and mix quality --profile merge ran the ADR judge with no finding. Gettext is not applicable.
  • Every new test carries a sabotage note; each mutation was applied, turned its test red on an assertion, and was restored byte-equal before the next.
  • Left for later, per the record's decision 9: non-scalar parameter values are written with inspect/1 (encoding undecided), JSON bodies and charset handling, _event.origin on inbound events, and a resumed session's location when the base URL moved. A registration without :base_url is refused on a fresh start; a resumed session does not rebuild _ioprocessors, so it is not refused there.

Refs: st-gje8

Implements ADR-0075 decisions 1-6 and 8 in the engine.

Statifier.Send.BasicHTTP is a registered send type: deliver/3 maps
event, namelist, params and content onto a POST (the event name in the
query string when the body is content), a missing target raises
error.communication, and perform/2 makes one attempt through an
injected transport, reporting a miss through failed_send/3. A delayed
send is the processor's timer and a cancel stops it. decode/1 is the
pure inbound half a front calls. The default transport is on OTP
:httpc; no dependency is added and the application list is unchanged.

Effects: a :send_types value may be {module, opts}. The registered set
keeps each module and its options, _ioprocessors asks the new optional
ioprocessors_entry/2 with the session id (both keys carry one
location), the planner adds :opts to a {module, opts} registration's
callback context only, and the recording writes options as strings.
Statifier.Testing.Case.test_scxml/5 takes a :send_types option, and a
loopback front on :inets httpd serves this repository's own runs.

Refs: st-gje8
ADR-0069 asks a registered processor to be idempotent on the ADR-0054
dedup key, and the Basic HTTP processor posts once per perform with no
memory. An Amendment to ADR-0075 (ruled by the operator, 2026-09-30)
makes it at-least-once with receiver deduplication.

Every POST, immediate or delayed, form or content body, carries the
key's eight components in an scxml-send-key header. decode/1 takes the
header's value as :send_key, sets the event's sendid from a well-formed
key and refuses a malformed one; the loopback front hands it over.

The httpc transport also checks that :ssl and :public_key can be
loaded and answers {:error, reason} when they cannot, instead of
crashing inside httpc under a pruned code path.

Refs: st-gje8
The decoder set an inbound event's sendid from the scxml-send-key
header's send id, which neither the ruling nor 5.10.1 supports: a
generated send id is not a value the sending entity specified, and
this repository sets sendid only for an author-named send.

decode/1 now sets no event field from :send_key and only refuses a
malformed one (400); a front deduplicates on the header's value
itself. The Amendment's decoder paragraph says so, and a test pins
that a POST whose key names a generated send id delivers an event
with no sendid. The Amendment's Status line now says which part the
operator ruled and which part is the record's.

Refs: st-gje8
@johnnyt
johnnyt merged commit 3ffcf9d into main Sep 30, 2026
1 check passed
@johnnyt
johnnyt deleted the st-gje8-basichttp-processor branch September 30, 2026 15:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant