Skip to content

Outbound HTTP design spec#275

Draft
aram356 wants to merge 4 commits into
feature/extensible-clifrom
docs/outbound-http-spec
Draft

Outbound HTTP design spec#275
aram356 wants to merge 4 commits into
feature/extensible-clifrom
docs/outbound-http-spec

Conversation

@aram356

@aram356 aram356 commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds the EdgeZero outbound HTTP design spec at docs/superpowers/specs/2026-05-21-outbound-http-design.md. Targets the PR #269 (feature/extensible-cli) baseline — the spec assumes the multi-store manifest, the edgezero_cli::adapter::execute(..) dispatcher, the expanded AdapterAction set, Adapter::provision / config-validation hooks, Spin SDK 6 / wasip2, and the demo command. Earlier appendices that quote the pre-#269 surface are explicitly flagged as historical.

Driving pattern

The spec is written against fan-out HTTP workloads — N concurrent outbound requests under a shared wall-clock deadline, results harvested in input order. The driving pattern is treated as a portable substrate; the spec deliberately does not name a single consumer.

Scope

Six driver requirements:

  • Portable outbound clientOutboundHttpClient trait with single send and concurrent send_all on every adapter (Axum, Cloudflare, Fastly, Spin). One handler source compiles unchanged across all four.
  • Deadline / timeout primitivesdispatch_budget(req, now) with explicit now snapshot, DEFAULT_NO_DEADLINE_BUDGET = 30s, DEADLINE_FAR_FUTURE = 7 days, BATCH_DISPATCH_SLACK_MAX = 25ms. Fastly's bounded-cooperative semantics are documented with precise overshoot bounds.
  • Bounded buffering — persistent vs transient memory accounting (max + sizeof(current_chunk) worst case, in-flight chunk size source-controlled), pre-append cap checks across inbound + outbound bounded drains. Batch model is Σᵢ request_bodyᵢ.len() + Σᵢ max_response_bytesᵢ.
  • Capability declaration — nine capabilities (outbound-http, outbound-deadlines, outbound-flexible-phase-budget, send-all-slot-isolation, streamed-upload-deadlines, lazy-streamed-response-passthrough, config-store, kv-store, secret-store). Enforcement runs as five pre-dispatch gates (one inside execute(..), siblings on run_provision / run_config_push / run_config_validate / run_demo).
  • Adapter contract tests — three tiers: Tier 1 (core + MockOutboundClient), Tier 2 (per-adapter translation), Tier 3 (runtime against a local mock origin). Adapter-specific mechanics (Fastly host timers, harvest behaviour, dynamic backend identity) are restricted to Tier 2/3 since Tier 1's mock has no analogue.
  • Canonical URI accessorsbackend_target() / host_authority() / sni_hostname() / cert_host() are the single source of truth for the host/port/SNI/cert split. Adapters MUST consume these, not re-derive from req.uri(). IP-literal HTTPS (RFC 6066 §3) is handled by sni_hostname() == None && cert_host() == Some(ip).

Out of scope (explicit non-goals)

  • No consumer-specific target logic in EdgeZero.
  • No new direct dependency on tokio, reqwest, fastly, worker, or spin-sdk in core or app/library crates.
  • No back-compat shims; renames are mechanical and downstream consumers migrate.

Process

The spec was revised through 49 review rounds. The non-normative resolution journal lives in Appendices A through AX. Appendix AR is the round-44 rebase snapshot, superseded by Appendices AS / AT / AU / AV / AW / AX (rounds 44–49).

Test plan

This is a docs-only PR.

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo test --workspace --all-targets
  • Docs CI (ESLint + Prettier on docs/)
  • Spec renders correctly in VitePress (no broken cross-refs to §1–§8 or Appendix anchors)

Adds the EdgeZero outbound HTTP design spec under
docs/superpowers/specs/2026-05-21-outbound-http-design.md. Targets the
PR #269 (feature/extensible-cli) baseline.

The spec covers six requirements:

- portable OutboundHttpClient trait with single send + concurrent
  send_all on every adapter (Axum, Cloudflare, Fastly, Spin)
- per-request and shared deadline / timeout primitives with a
  documented dispatch budget and bounded-cooperative semantics on
  Fastly
- bounded buffering with explicit persistent vs transient memory
  accounting and pre-append cap checks
- manifest-driven [capabilities] declaration (nine capabilities total)
  with pre-dispatch enforcement gates at five CLI entry points
- adapter contract test plan in three tiers (core mock, per-adapter
  translation, runtime)
- four canonical URI accessors (backend_target, host_authority,
  sni_hostname, cert_host) so adapters share one canonical
  host/port/SNI/cert split

Revised through 49 review rounds; non-normative resolution journal
lives in Appendices A through AX.
@aram356 aram356 changed the title Outbound HTTP design spec (midbid driver) Outbound HTTP design spec Jun 8, 2026
@aram356 aram356 marked this pull request as draft June 8, 2026 06:55
aram356 added 2 commits June 8, 2026 00:02
Removes references to the originally-named driving consumer and the
specific external protocol used as motivation:

- midbid       → "the driving pattern" / generic
- Prebid-style → "fan-out-style"
- OpenRTB      → "the external batch protocol"
- bidder       → "target"
- auction      → "fan-out batch"
- tmax         → "batch deadline"

The technical motivation (N concurrent outbound requests under a shared
wall-clock deadline, results harvested in input order, small response
bodies, homogeneous-budget common case) is preserved; only the named
consumer and its protocol are scrubbed so the spec reads as a portable
substrate rather than a single-consumer design.

Section §3.3.2 retitled "Mapping an external batch deadline to EdgeZero
deadlines"; status header gains a "Driving pattern" line in place of
the old "Driving consumer" pointer.
Spec previously claimed Fastly behaviour that did not match the actual
SDK / public API. This commit corrects the four normative claims, adds
an app-facing consuming body accessor, and records the corrections in
Appendix AY.

Findings addressed:

- lazy-streamed-response-passthrough downgraded Native -> BestEffort
  on Fastly. `Response::with_streaming_body` does not exist;
  `Response::stream_to_client()` is the actual API and is documented
  as incompatible with `#[fastly::main]`. Default scaffold falls back
  to buffered passthrough; lazy passthrough requires the non-main
  entry-point template tracked in new section 8 risk 12.

- NameInUse semantics rewritten. Fastly's session-uniqueness rule is
  unconditional; the previous "identical name + identical properties
  is a re-registration that returns Ok" carve-out was false. SDK's
  `Backend::from_str(name)` returns a handle only and exposes no
  registered properties, so a NameInUse on a name not in this
  adapter's collision map is now an explicit fail-closed internal
  error rather than a silent property-trust fallback.

- between_bytes_timeout is receive-side only per Fastly's Backend
  API docs. The previous claim that it bounded guest-to-origin
  writes is removed; the streamed-upload host-write phase is
  downgraded to BestEffort with the cooperative inter-chunk check
  as the only adapter-side bound.

- Streamed-upload response overshoot tightened from per-chunk
  accumulator to closed-form bound: first_byte_ms (headers wait)
  plus one between_bytes_timeout (worst-case first-body-chunk
  read), one-shot. Footnote 1 single-send section + section 5.4
  test row updated.

- OutboundResponse::into_body() added as the app-facing consuming
  accessor for streamed-response orchestration. The send_all
  rustdoc recommends single send + futures::join_all + into_body()
  on Axum/CF/Spin as the canonical path; into_parts(..) stays
  adapter-facing.

Appendix AY records the five resolutions. Status header bumped to
"rounds 1-50, Date: 2026-06-08"; superseded-AR pointer extended to
include AY.
@aram356

aram356 commented Jun 8, 2026

Copy link
Copy Markdown
Contributor Author

Round 50 — Fastly SDK correctness pass (commit 647489a)

Reviewer flagged four normative claims about Fastly that didn't match the actual SDK / public API. All fixes verified against docs.rs/fastly and Fastly's public Backend API docs before edit.

HIGH — lazy-streamed-response-passthrough on Fastly: downgraded NativeBestEffort. Response::with_streaming_body does not exist (only on Request). Response::stream_to_client() is the real API but is incompatible with #[fastly::main]. Default scaffold falls back to buffered passthrough; the non-#[fastly::main] entry-point template is tracked in new §8 risk 12.

HIGH — NameInUse semantics: rewritten. Fastly's session-uniqueness rule is unconditional — no "identical re-registration returns Ok" carve-out. Backend::from_str(name) returns a handle only, no property inspection. NameInUse on a name not in this adapter's collision map is now an explicit fail-closed EdgeError::internal.

MEDIUM — between_bytes_timeout direction: dropped the false write-side claim per Fastly's public Backend API docs (receive-side only). Streamed-upload host-write phase downgraded to BestEffort; cooperative inter-chunk check is the only adapter-side bound.

MEDIUM — Streamed-upload response overshoot: tightened from per-chunk accumulator to closed-form bound — first_byte_ms (headers wait) + one between_bytes_timeout (worst-case first-body-chunk read), one-shot. Footnote 1 single-send section and new §5.4 test row added.

LOW — App-facing consuming body accessor: added OutboundResponse::into_body(self) -> Body. send_all rustdoc now recommends single send + futures::join_all + into_body() as the canonical streamed-response orchestration path on Axum/CF/Spin.

Appendix AY records all five resolutions. Status bumped to rounds 1–50.

Five round-50 carry-over findings:

- Early section 4.3 dynamic-backend prose still preserved the stale
  identical-properties-re-register carve-out, contradicting the corrected
  step-5 algorithm. Rewritten in place to match the unconditional
  session-uniqueness contract. Two historical appendix entries (round-37
  in Appendix AK) marked superseded by Appendix AY.

- Fastly buffered-fallback for lazy passthrough named max_response_bytes
  as the cap, but that per-request cap is unavailable at response-converter
  time. Added FASTLY_RESPONSE_STREAM_BUFFER_BYTES adapter-level constant
  (mirrors AXUM_RESPONSE_STREAM_BUFFER_BYTES). Three section 5.4 rows
  rebucketed so Fastly is no longer in the CF/Spin lazy group; new
  Axum-and-Fastly buffered-fallback row carries both adapter constants.

- Residual between_bytes_timeout write-side claim removed from the
  remaining section 5.4 stalled-upload mechanics row and from section 8
  risk 7. Fastly write phase is BestEffort uniformly now; the public
  Backend API docs are cited as the source.

- Spin host-write race rewritten against actual WASI output-stream
  semantics. Old wording said each write() is raced against a timer;
  WASI write() is nonblocking and readiness-polled, so the implementable
  pattern is subscribe-pollable + futures::select! vs timer + nonblocking
  check_write() + write() within the permitted byte count.

- Typo: "docsare migrated" -> "docs are migrated" in section 1.3 non-goals.

Status header bumped to rounds 1-51; AR-superseded pointer extended to
include AZ.
@aram356

aram356 commented Jun 8, 2026

Copy link
Copy Markdown
Contributor Author

Round 51 — round-50 carry-overs + Spin WASI write mechanics (commit 8df9d80)

Five follow-up findings from the round-50 reviewer pass. All verified against the cited external docs before edit.

HIGH — Fastly dynamic-backend semantics contradictory across sections. Round 50 fixed the §4.3 step-5 algorithm but the earlier Dynamic backends introductory paragraph still preserved the false "identical name + identical properties re-registers / returns Ok" carve-out. Rewritten in place: session-uniqueness is unconditional per the SDK; EdgeZero owns the entire uniqueness story at the guest layer via the adapter-local cache; NameInUse outside that cache is fail-closed EdgeError::internal. Two historical Appendix-AK entries marked "Superseded by Appendix AY."

MEDIUM — Fastly buffered-fallback named an unavailable cap; §5.4 still bundled Fastly with CF/Spin. Added FASTLY_RESPONSE_STREAM_BUFFER_BYTES (default 16 MiB, mirrors AXUM_RESPONSE_STREAM_BUFFER_BYTES). §5.4 lazy-passthrough rows split: (a) CF/Spin yield-first-bytes row excludes Fastly; (b) CF/Spin mid-stream abort row excludes Fastly; (c) new Axum-and-Fastly buffered-fallback row with both adapter constants named.

MEDIUM — Residual between_bytes_timeout write-side claims. The "stalled streamed-upload mechanics differ per adapter" §5.4 row and §8 risk 7 both still claimed the Fastly between-bytes-timeout bounded guest-to-origin writes. Scrubbed both — Fastly's write phase is BestEffort uniformly. §8 risk 7 retitled and rewritten to track the symmetric "if Fastly adds a documented guest-write timeout in the future" follow-up.

MEDIUM — Spin host-write race mechanically wrong vs WASI. Old wording raced each OutgoingBody::write against a timer; WASI output-stream is nonblocking + readiness-polled. Rewritten as the four-step pattern: subscribe() pollable → futures::select! pollable-ready vs monotonic-clock timer → on timer-win drop the handle + return gateway_timeout → on pollable-win call nonblocking check_write() for permitted byte count + write() within that bound, looping. §5.4 row updated to match.

LOW — Typo: docsare migrateddocs are migrated in §1.3.

Appendix AZ records all five resolutions; status bumped to rounds 1–51.

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