Adds the Basic HTTP Event I/O Processor - #353
Merged
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
:httpcdefault, 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: aStatifier.Send.Processor.deliver/3mapsevent,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'serror.communicationfor a send with no target.perform/2makes one attempt through the transport and reports a transport error or a non-2xx status throughStatifier.Session.failed_send/3(decision 8, point d); a delayed send is the processor's timer and a<cancel>stops it (decision 9).decode/1is the pure inbound decoder of decision 5 (the first_scxmleventname, query before body, elseHTTP.POST; form values and other bodies through the text rung).ioprocessors_entry/2writes the location, the base URL,/and the session id, and refuses a registration without:base_urlwhen the session starts.scxml-send-keyheader, its eight components joined by/(session scope and send id percent-encoded, counters decimal,ownerspelledonentry.S.B,onexit.S.B,finalize.S.Bortransition.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/1takes the header's value as:send_key, sets no event field from it (an inbound event'ssendidstays 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) andStatifier.Send.BasicHTTP.Transport.Httpc, always compiled, TLS verified against the system CA store, bounded by timeouts, starting:inetsand:sslitself on first use and checking that:ssland:public_keycan be loaded, answering{:error, reason}when they cannot.mix.exs,mix.lockand the application list are unchanged.{module, opts}:send_typesvalue (decision 8, point b):Statifier.Send.Typeskeeps each type's module and options;Statifier.Evaluator.SystemVariables.initial/3asks the new optionalioprocessors_entry/2callback with the session id and the options (decision 3; a module exporting only/1is asked as before); the planner's lookup adds:optsto the context ofdeliver/3andcancel/2for a{module, opts}registration only;Statifier.Session.Recordingwrites the options as strings.Statifier.Testing.Case.test_scxml/5takes a:send_typesoption; a call without it starts its session as before.Mix.Statifier.BasicHTTPFront: the loopback front on:inetshttpd for this repository's own runs, answering by the status rule (204, 405 withAllow: POST, 400, 404).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_typesstaysnil,_ioprocessorsholds 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:
lib/mix/statifier/(repository tooling, not packaged) rather thantest/support/, so the corpus host can start it outside the test environment; the record lists it among the changes inlib/.perform/2still receives the plan context without:opts(the session performs a{:handler, module, payload}instruction knowing only the module), sodeliver/3carries the transport in the payload it plans.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.{:not_utf8, part}, answered 400); a non-form body takes the query string's event name only.:httpcand:public_keycalls 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.:httpcadapter answer{:error, {:not_loadable, module, reason}}when:sslor:public_keycannot be loaded; before it, a POST under a Mix task whose code path had been pruned crashed inside httpc even for anhttp:URL (reproduced withmix runin the dev environment; the same run now answers the error).decode/1set the inbound event'ssendidfrom 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 nosendid.{module, opts}writes each option key as a string and each atom value as{:atom, name}; unresolvable names join{:unknown_handler_modules, names}.Notes
mix qualityis green on the head (Tests 3,121 of 3,121, 96.3% coverage; Dialyzer, Doctor, Credo, ADR guard all green), andmix quality --profile mergeran the ADR judge with no finding. Gettext is not applicable.inspect/1(encoding undecided), JSON bodies and charset handling,_event.originon inbound events, and a resumed session's location when the base URL moved. A registration without:base_urlis refused on a fresh start; a resumed session does not rebuild_ioprocessors, so it is not refused there.Refs: st-gje8