fed-sx-m2: Step 8e — httpc:request/4 BIF wrapper (+ 10 tests)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 37s

Closes the BIF half of Step 8. Native http-request primitive landed
in architecture via the fed-prims merge (the m2 plan's Blocker #2),
so the briefing-allowed-exception wrapper in lib/erlang/runtime.sx
can finally be wired.

Marshalling at the BIF boundary:
  Url     : Erlang binary -> SX string (byte-list -> integer->char).
  Method  : Erlang atom upcased ('get -> "GET") for HTTP-wire
            convention, or Erlang binary passes through verbatim.
  Headers : Erlang proplist -> SX dict via er-proplist-to-dict.
  Body    : Erlang binary -> SX string.

Result {:status :headers :body} marshalled back to Erlang
  {ok, Status::integer,
       Headers::proplist (binary-keyed via er-of-sx-deep),
       Body::binary (char->integer over the SX string)}.

Bad arg shapes (non-binary URL or body) raise error:badarg; native
DNS / connect / bad-URL failures surface as Erlang error markers
that the caller can catch.

Test: next/tests/httpc_request.sh 10/10
  - registration under httpc/request/4
  - BIF marked non-pure
  - wrong-arity (/1) absent from registry
  - badarg on non-binary URL
  - badarg on non-binary body
  - live GET against `python3 -m http.server` -> Status 200
  - body bytes match "hello from python\n"
  - headers come back as proplist (is_list/1 = true)
  - 404 path -> {ok, 404, ...} (not an error tuple)
  - method passed as binary works

URLs spelled out as byte-list <<104,116,116,p,...>> binaries since
the parser truncates <<"..."> string-literal binaries (same
workaround backfill_drain.sh uses for inbox paths).

Plan: 8e ticked; Blocker #2 marked RESOLVED with the merge that
unblocked it referenced. Step 8f (live HTTP dispatch through
delivery_worker) and Step 10c (peer-actor doc fetch) are now
unblocked.

No-regression gates green: Erlang conformance 761/761,
http_multi_actor 44/44, follower_graph 18/18, follow_lifecycle 9/9,
backfill 20/20, backfill_drain 6/6, http_listen_bif 5/5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 10:44:25 +00:00
parent 070986913d
commit bd2c61367d
3 changed files with 266 additions and 17 deletions

View File

@@ -594,12 +594,18 @@ a dead-letter list visible via `/admin/dead-letter`.
in `delivery_dispatch.sh` covering single-peer enqueue,
two-peer fan-out, missing-worker skip, no-flag no-op,
FIFO append across two publishes, empty delivery_set no-op.
- [ ] **8e** — `httpc:request/4` BIF wrapper. **Blocker:** the
briefing assumed a native `http-request` primitive existed in
`bin/sx_server.ml`; on inspection there's only `http-listen`.
The native http-CLIENT primitive belongs to `loops/fed-prims`
(host primitives loop). Blockers entry below. m2 work
continues with the in-process flow until the native lands.
- [x] **8e** — `httpc:request/4` BIF wrapper. ~~Blocker~~ resolved:
loops/fed-prims merged into architecture, native `http-request`
primitive available. Wrapper at `lib/erlang/runtime.sx`
(briefing-allowed-exception scope) marshals Erlang
`(Url::binary, Method::atom|binary, Headers::proplist, Body::binary)`
→ SX `(http-request method url headers body)` → Erlang
`{ok, Status::integer, Headers::proplist, Body::binary}`.
Atom methods are upcased (`get` → `"GET"`) for HTTP-wire convention;
binaries pass through verbatim. Test: `next/tests/httpc_request.sh`
10/10 pass — registration, badarg validation, live GET 200,
body bytes match, headers proplist shape, 404 surfaces as ok-tuple,
binary method works.
- [ ] **8f** — Real HTTP dispatch through the BIF + content-type
wiring. dispatch_fn for live use becomes a closure over the
peer URL that calls `httpc:request/4` with the signed envelope
@@ -1026,17 +1032,16 @@ proceed.
re-running on the unmodified m1 closeout HEAD.
2. **Native `http-request` (HTTP client) primitive missing** —
discovered during Step 8e prep. The fed-sx-m2 briefing
("Substrate available to you" §) claimed: "Native HTTP client
primitive (registered in `bin/sx_server.ml`): `http-request` —
exposed at the SX layer, currently native-only." On inspection
`bin/sx_server.ml` only registers `http-listen`; there is no
`http-request` registration. The HTTP client primitive belongs
to `loops/fed-prims` (host primitives loop) per the
one-primitive-loop-per-substrate convention. m2's Step 8e
wrapper (`httpc:request/4` BIF in `lib/erlang/runtime.sx`)
can land in a 1-line follow-up once the native exists; m2
work continues with 8b-pure / 8c / 8d in the in-process flow.
~~discovered during Step 8e prep~~ **RESOLVED 2026-06-07** by
the user-authorized `loops/fed-prims` → `architecture` merge.
The primitive now registers at `bin/sx_server.ml:868+` with
signature `(http-request meth url headers body)` returning a
`{:status :headers :body}` dict and raising `Eval_error` on
DNS / connect / bad URL. Step 8e wired the Erlang-side BIF
wrapper around it (`httpc:request/4`); see Progress log
entry for marshalling details. Step 8f (live HTTP dispatch
through `delivery_worker`) and Step 10c (peer-actor doc
fetch in `peer_actors`) are now unblocked.
3. **`erlang:send_after`-style timer primitive** — discovered
during Step 8b prep. The retry loop needs a way for the
@@ -1055,6 +1060,39 @@ proceed.
Newest first.
- **2026-06-07** — Step 8e (closes the BIF half of Step 8;
live HTTP dispatch in 8f next): `httpc:request/4` BIF wrapper
landed in `lib/erlang/runtime.sx` (briefing-allowed-exception
scope). Marshalling: Erlang URL binary → SX string via
`(list->string (map integer->char (get url :bytes)))`; Erlang
atom method → upcased name (`get` → `"GET"`) for HTTP wire
convention; binary method passes through verbatim; headers
proplist → SX dict via existing `er-proplist-to-dict`; body
binary → SX string. Result `{:status :headers :body}` marshalled
back to Erlang `{ok, Status, Headers::proplist, Body::binary}`
via `er-of-sx-deep` on headers (which produces the binary-keyed
proplist `er-dict-to-header-proplist` shape) and
`(er-mk-binary (map char->integer (string->list body)))` for
body. Non-binary URL / body raise `error:badarg`; the native
primitive raises `Eval_error` on DNS / connect / bad URL which
surfaces as an Erlang error marker the caller can catch.
Blockers #2 (native http-request primitive) entry updated:
RESOLVED by the loops/fed-prims → architecture merge that the
user authorized. Test: `next/tests/httpc_request.sh` 10/10 —
5 registration / validation cases (registration under
`httpc/request/4`, non-pure flag, no /1 arity, badarg on
non-binary URL, badarg on non-binary body) plus 5 live
roundtrip cases against a background `python3 -m http.server`
(Status 200, body bytes match `hello from python\n`, headers
proplist shape, 404 surfaces as `{ok, 404, ...}` not as an
error tuple, method passed as binary works). Adjacent gates:
Erlang conformance 761/761, http_multi_actor 44/44, follower_
graph 18/18, follow_lifecycle 9/9, backfill 20/20,
backfill_drain 6/6, http_listen_bif 5/5 — all green; pre-
existing cold-startup timeout sensitivity on http_get_format
(120s internal) and nx_kernel_pure (240s internal) confirmed
with git stash to NOT be caused by this change.
- **2026-06-07** — Step 9c (closes Step 9): Follow → Accept →
backfill drain (in-process). `maybe_auto_accept/3` now calls
`maybe_backfill/3` after the Accept publish: when