Migrate handler layer to HTTP types (PR 12)#624
Conversation
Rename crates/common → crates/trusted-server-core and crates/fastly → crates/trusted-server-adapter-fastly following the EdgeZero naming convention. Add EdgeZero workspace dependencies pinned to rev 170b74b. Update all references across docs, CI workflows, scripts, agent files, and configuration.
Introduces trusted-server-core::platform with PlatformConfigStore, PlatformSecretStore, PlatformKvStore, PlatformBackend, PlatformHttpClient, and PlatformGeo traits alongside ClientInfo, PlatformError, and RuntimeServices. Wires the Fastly adapter implementations and threads RuntimeServices into route_request. Moves GeoInfo to platform/types as platform-neutral data and adds geo_from_fastly for field mapping.
…o-pr2-platform-traits
- Defer KV store opening: replace early error return with a local UnavailableKvStore fallback so routes that do not need synthetic ID access succeed when the KV store is missing or temporarily unavailable - Use ConfigStore::try_open + try_get and SecretStore::try_get throughout FastlyPlatformConfigStore and FastlyPlatformSecretStore to honour the Result contract instead of panicking on open/lookup failure - Encapsulate RuntimeServices service fields as pub(crate) with public getter methods (config_store, secret_store, backend, http_client, geo) and a pub new() constructor; adapter updated to use new() - Reference #487 in FastlyPlatformHttpClient stub (PR 6 implements it) - Remove unused KvPage re-export from platform/mod.rs - Use super::KvHandle shorthand in RuntimeServices::kv_handle()
- Split fastly_storage.rs into storage/{config_store,secret_store,api_client,mod}.rs
- Add PlatformConfigStore read path via FastlyPlatformConfigStore::get using ConfigStore::try_open/try_get
- Add PlatformError::NotImplemented variant; stub write methods on FastlyPlatformConfigStore and FastlyPlatformSecretStore
- Add StoreName/StoreId newtypes with From<String>, From<&str>, AsRef<str>
- Add UnavailableKvStore to core platform module
- Add RuntimeServicesBuilder replacing 7-arg constructor
- Migrate get_active_jwks and handle_trusted_server_discovery to use &RuntimeServices
- Update call sites in signing.rs, rotation.rs, main.rs
- Add success-path test for handle_trusted_server_discovery using StubJwksConfigStore
- Fix test_parse_cookies_to_jar_empty typo (was emtpy)
- Make StoreName and StoreId inner fields private; From/AsRef provide all needed construction and access - Add #[deprecated] to GeoInfo::from_request with #[allow(deprecated)] at the three legacy call sites to track migration progress - Enumerate the six platform traits in the platform module doc comment - Extract backend_config_from_spec helper to remove duplicate BackendConfig construction in predict_name and ensure - Replace .into_iter().collect() with .to_vec() on secret plaintext bytes - Remove unused bytes dependency from trusted-server-adapter-fastly - Add comment on SecretStore::open clarifying it already returns Result (unlike ConfigStore::open which panics)
…into feature/edgezero-pr10-abstract-logging-initialization
…into feature/edgezero-pr11-utility-layer-migration-v2 Resolve conflicts by adopting PR10's ec_id naming throughout. cookies.rs set_ec_cookie/expire_ec_cookie retain Response<EdgeBody> types to satisfy migration_guards; Fastly-typed callers route through compat bridge. Remove synthetic.rs (deleted in PR10) and its migration_guards entry.
cookies.rs set_ec_cookie/expire_ec_cookie now take Response<EdgeBody> to satisfy the migration_guards invariant. registry.rs and publisher.rs call the Fastly-typed equivalents in compat instead. Remove synthetic.rs entry from migration_guards (file deleted in PR10).
…into feature/edgezero-pr11-utility-layer-migration-v2
…feature/edgezero-pr12-handler-layer-types
aram356
left a comment
There was a problem hiding this comment.
Summary
Round-4 review. Round-3's blocking finding (to_fastly_request_ref clobbering multi-value headers) is resolved at compat.rs:95 with a regression test at compat.rs:478. CI is green.
Two new concerns this round: (1) the round-3 ♻️ ask for 413 regression tests on the four endpoints whose caps were introduced in this PR is still unaddressed — only the proxy endpoints got sign_rejects_oversized_body/rebuild_rejects_oversized_body. (2) Two doc-vs-code mismatches in compat.rs — the # Panics blocks describe a panic site that doesn't exist (URL parsing has a silent fallback to /), while the real panic site (header construction) is undocumented.
Blocking
🔧 wrench
- No 413 regression tests for
/verify-signature,/admin/keys/{rotate,deactivate}(request_signing/endpoints.rs:103, :254, :381). Round-3 explicitly asked. See inline. - No 413 regression test for
/auction; file has no#[cfg(test)]module (auction/endpoints.rs:45). See inline.
Non-blocking
🤔 thinking
# Panicsdoc claims URL-parse panic that cannot occur (compat.rs:42-44, compat.rs:56-58). Doc/code mismatch. See inline.- Silent URL fallback to
/masks malformed URIs (compat.rs:18-21).build_http_requestusesunwrap_or_else(|_| http::Uri::from_static("/")). Realistically unreachable given Fastly pre-validation, but produces silent misrouting (catch-all hits the publisher origin at main.rs:215) if it ever fires. Either document why the fallback is reachable-but-safe, or assert the invariant withexpect("should parse Fastly-validated URL").
♻️ refactor
65536literal duplicated 4× in proxy.rs (proxy.rs:884, :887, :1007, :1010) — the only handler in this PR not using named constants. See inline.String::from_utf8(body_bytes.to_vec())allocates twice (proxy.rs:892, :1015). See inline.- Body-size cap pattern duplicated 6× (carry-over from round 3). Same
if body.len() > MAX { return Err(RequestTooLarge { format!(...) }) }shape at proxy.rs:884, proxy.rs:1007, auction/endpoints.rs:45, and three times in request_signing/endpoints.rs (103, 254, 381). A smallenforce_max_body_size(bytes, limit, what)helper inhttp_utilwould centralize the message format and remove ~50 lines.
🌱 seedling
DEFAULT_PUBLISHER_FIRST_BYTE_TIMEOUT = 15sstill hardcoded (publisher.rs:21). Carry-over from round 1 — worth a config knob during PR-15 boundary cleanup.- Body-size caps still fire after
req.take_body_bytes()materializes (compat.rs:46). Carry-over from round 3 — caps protect parse allocations but not the receive allocation. PR-15 streaming-aware shim should land alongside boundary removal so the protection isn't quietly downgraded.
📝 note
platform_response_to_fastlyis now the only diverging conversion (compat.rs:245-264). ReturnsErron streaming bodies instead ofto_fastly_response's silent-drop. The asymmetry is correct for orchestrator use, but worth pinning in the PR-15 plan so streaming closes consistently.
CI Status
- browser integration tests: PASS
- integration tests: PASS
- prepare integration artifacts: PASS
…feature/edgezero-pr12-handler-layer-types
aram356
left a comment
There was a problem hiding this comment.
Summary
Re-review of the handler-layer HTTP-types migration. The migration is structurally clean, but two concerns block merge: (1) the publisher streaming dispatch added in #562 is regressed — PublisherResponse::Stream now buffers the entire pipeline output instead of writing chunks via stream_to_client(), and (2) cargo test --workspace fails on the current head because the debug_assert! in compat::to_fastly_response (introduced in PR 11 and inherited here) contradicts the test that exercises the documented silent-drop fallback. CI did not catch (2) because PR 624's base branch causes test.yml and format.yml to skip.
Blocking
🔧 wrench
-
Streaming regression on the publisher fallback path. Before this PR,
Ok(PublisherResponse::Stream { … })inadapter-fastly/src/main.rscalledresponse.stream_to_client()and wrote chunks viastream_publisher_body. After this PR,resolve_publisher_responsematerializes the entire pipeline output into aVec<u8>, setsContent-Length, replaces the body, and the adapter sends viacompat::to_fastly_response(response).send_to_client(). The whole publisher response is now buffered in WASM memory.Observable: the
PublisherResponse::Streamenum variant becomes equivalent toPublisherResponse::Buffereduntil streaming is restored. TTFB is delayed by the full pipeline pass, and WASM memory pressure scales with body size.Structural cause:
compat::to_fastly_responsecannot move anEdgeBody::Streaminto afastly::StreamingBody— there is no streaming bridge across the compat shim. Two reasonable directions:- Preferred: Keep the streaming dispatch in the adapter. Have
route_requestreturnResult<HandlerOutcome, …>whereHandlerOutcome::Streaming(…)carries the unbuffered body+params, andmain()opensstream_to_client()on the converted Fastly response and callsstream_publisher_bodyagainst it. Honest "no behavior regressions" version of PR 12. - Alternative: Block the streaming arm and route everything through
BufferedProcesseduntil PR 15 introduces a streaming body bridge — but explicitly call this out as a known regression in the PR description and add a tracking issue.
No test today asserts streaming behavior. The publisher tests (
stream_publisher_body_*) all useVec<u8>outputs and don't distinguish streamed-to-client from buffered-then-sent. So the regression is entirely silent. - Preferred: Keep the streaming dispatch in the adapter. Have
-
cargo test --workspacefails:compat::tests::to_fastly_response_with_streaming_body_produces_empty_bodypanics on adebug_assert!that contradicts the test.In
crates/trusted-server-core/src/compat.rs(line ~138), the function assertsmatches!(&body, EdgeBody::Once(_)). The test below it (line ~676) constructs anEdgeBody::Streamand asserts the body comes out empty — so the test panics incargo testdebug builds.Both the
debug_assert!and the test were introduced in PR 11 (commit76df6f8), so PR 624 inherits the contradiction rather than introducing it — but it materializes here because (a) the PR's test plan checkbox[x] cargo test --workspacedoes not pass on the current head, and (b) when this stack lands onmainand a downstream PR opens, the standardtest.ymlworkflow will fail. The PR 11 review can fix it upstream, or this PR can apply the fix.Fix (preferred — the warn-and-empty fallback is documented behavior):
pub fn to_fastly_response(resp: http::Response<EdgeBody>) -> fastly::Response { // … build headers … match body { EdgeBody::Once(bytes) => { if !bytes.is_empty() { fastly_resp.set_body(bytes.to_vec()); } } EdgeBody::Stream(_) => { log::warn!("streaming body in compat::to_fastly_response; body will be empty"); } } fastly_resp }
Alternative: keep the
debug_assert!and gate the test on#[cfg(not(debug_assertions))]. But the warn-log already covers misuse and the documented fallback path should be exercised by tests.
Non-blocking
🤔 thinking
- Silent body drop on
EdgeBody::Streamincompat. Bothto_fastly_requestandto_fastly_responselog a warning and emit an empty body when handed a streamingEdgeBody. Combined with the streaming-dispatch removal above, this means a future caller that tries to stream through the compat boundary will silently see truncated bodies. Promote the misuse to aResultreturn (the Fastly adapter'sedge_request_to_fastlyalready does this), or add a comment listing the audited non-streaming call sites so the constraint is verifiable. Not in PR 12's diff but the streaming-regression fix will reintroduce the constraint.
♻️ refactor
bytes.to_vec()copies the full body across the compat boundary.compat.rs:144-146andcompat.rs:81-84memcpy the body. For large publisher responses this is unavoidable O(N) at the shim. Worth a one-line comment that this cost goes away with PR 15 so future readers don't try to optimize inside the shim. (Not in PR 12's diff.)
🌱 seedling
- Sync between
route_requestadmin routes andSettings::ADMIN_ENDPOINTSis comment-only.main.rs:166carries a "Keep in sync with Settings::ADMIN_ENDPOINTS" comment. A test that compares the constant against the routes registered inroute_requestwould prevent future drift. Follow-up issue.
📝 note
- CI workflow gate gap.
.github/workflows/test.ymland.github/workflows/format.ymltrigger only onpull_request: branches: [main]. PR 624 targets the PR 11 feature branch, so cargo test, fmt, and clippy never ran on this PR. Onlyintegration-tests.ymlran (and passed). This is how the failingcompattest stayed invisible. Out of scope for PR 12 to fix — but the consequence (a regression that will only surface when this lands on main and a downstream PR opens) is in scope. Either drop thebranches: [main]filter for the EdgeZero stack PRs, orworkflow_dispatchthe test workflow against each stack head.
CI Status
- fmt: PASS (locally)
- clippy: PASS (locally)
- rust tests: FAIL locally (
compat::tests::to_fastly_response_with_streaming_body_produces_empty_body); not run on CI for this PR - integration tests: PASS (CI)
- browser integration tests: PASS (CI)
Blocking fixes: - Remove debug_assert! in compat::to_fastly_response that contradicted the documented silent-drop fallback and caused to_fastly_response_with_streaming_body_produces_empty_body to panic in debug builds - Restore streaming dispatch for PublisherResponse::Stream, which was regressed by the PR 11 compat shim. Introduce HandlerOutcome enum so route_request can signal streaming intent back to the adapter. main() handles HandlerOutcome::Streaming via to_fastly_response_skeleton + stream_to_client + stream_publisher_body directly, avoiding the Vec<u8> buffer that delayed TTFB and scaled WASM memory with body size. Non-blocking fixes: - Add to_fastly_response_skeleton for headers-only fastly response conversion - Add geo-401 comment: skip geo lookup for unauthenticated callers - Add audited-callers comment on EdgeBody::Stream arms in compat - Add O(N) / PR 15 comment on bytes.to_vec() calls - Deduplicate auction/publisher status extraction in route_tests via outcome_status helper
Resolve conflicts in main.rs, compat.rs, auction/endpoints.rs, cookies.rs, edge_cookie.rs, migration_guards.rs, publisher.rs, and request_signing/endpoints.rs. Includes main's JA4 debug endpoint, Sourcepoint integration, and Prebid bid param override rules. Removed unused try_build_ec_cookie_value and get_ec_id_from_http_request stubs surfaced by clippy after merge.
StreamingBody in fastly 0.11.12 has no Drop impl — dropping it without calling finish() leaves the chunked HTTP stream with no terminal chunk, causing clients to receive "unexpected EOF during chunk size line". Match on stream_publisher_body result and call finish() in both the success and error paths so the response is always properly terminated.
aram356
left a comment
There was a problem hiding this comment.
Summary
Round-6 review (worktree). The handler-layer migration to http::{Request,Response}<EdgeBody> is structurally sound, the round-5 blocking findings (streaming regression, contradictory debug_assert! vs test, multi-value header preservation, 413 regression tests, # Panics doc mismatch) are all resolved, and cargo test --workspace (1016 tests), cargo clippy --workspace --all-targets --all-features -- -D warnings, and cargo fmt --all -- --check pass locally.
Two new correctness regressions block merge:
- The
streaming_body.finish()fix in commit cd4c621 silently completes truncated responses on mid-stream errors — a regression from the priordrop(streaming_body)semantics that the original adapter comment explicitly relied on for client-visible truncation. - The
outcome.status() == UNAUTHORIZEDgate strips geo headers from origin-forwarded 401s, conflating two distinct sources of 401 and losing observability signal without addressing the original threat model.
Both are introduced by this PR's restructuring of the adapter entry and the new streaming dispatch arm. Neither was caught by prior rounds because the round-5 review concluded before commit cd4c621 (the streaming fix) landed, and the geo-status gate was added in this PR's rewrite of route_request.
A handful of non-blocking concerns and carry-overs from rounds 3/4 round out the review.
Blocking
🔧 wrench
streaming_body.finish()on error masks truncation: previously the error path dropped the streaming body so the client saw an EOF (truncated response). Commit cd4c621 made it callfinish()in both arms, cleanly closing chunked encoding even on mid-pipeline failures. Client now sees a successful-looking partial response. See inline atmain.rs:188.outcome.status() == UNAUTHORIZEDconflates our 401 with origin-forwarded 401: geo is stripped from any response that happens to be 401, not just ourenforce_basic_authchallenge. See inline atmain.rs:146.
Non-blocking
🤔 thinking
stream_publisher_bodydoc claimsasyncbut the function is sync (publisher.rs:402-405). The async/block_onstory applies tohandle_publisher_request, not this function. See inline.to_fastly_request_refsilently drops body (compat.rs:102). Audited safe today, but no compile-time signal. See inline.
♻️ refactor
- Body-size cap pattern duplicated 6× (carry-over from rounds 3 & 4).
proxy.rs:886,:1009,auction/endpoints.rs:45,request_signing/endpoints.rs:103,:254,:381. A smallenforce_max_body_size(bytes, limit, what)helper inhttp_utilwould centralize the message format. See inline.
🌱 seedling
DEFAULT_PUBLISHER_FIRST_BYTE_TIMEOUT = 15sstill hardcoded (publisher.rs:21). Carry-over from round 1.- Body-size caps still fire after
req.take_body_bytes()materialises (compat.rs:51). Carry-over from round 3 — receive-side allocation unprotected.
📝 note
- No test covering
to_fastly_response_skeleton(compat.rs:167). The function is reachable from a single call site (the streaming dispatch inmain.rs:169). A test that round-trips status + headers (minus body) would protect the streaming-dispatch contract. Not blocking — the function is short andstream_publisher_body_*tests indirectly exercise the streaming path.
Resolved from prior rounds
- ✅ Round-5 streaming regression —
HandlerOutcome::Streaming+to_fastly_response_skeleton+stream_publisher_bodyrestores the dispatch (main.rs:163-191, compat.rs:167) - ✅ Round-5
cargo test --workspacefailure — debug_assert removed fromto_fastly_response(compat.rs:148-153) - ✅ Round-4 doc/code mismatch on
# Panics— compat.rs:45-49 and :61-65 now match the URL-parse-with-fallback behaviour - ✅ Round-4 missing 413 tests on
/verify-signature,/admin/keys/*,/auction—request_signing/endpoints.rs:926, 944, 962,auction/endpoints.rs:151 - ✅ Round-3
to_fastly_request_refmulti-value header bug — compat.rs:105 usesappend_header, regression test at compat.rs:509 - ✅ Round-3
platform_response_to_fastlyduplicated into orchestrator.rs — now lives only in compat.rs:276, returnsErron streaming - ✅ Round-1 substring matching in migration_guards.rs — now uses
\b…\bregex with comment stripping (migration_guards.rs:48-51) - ✅ Round-1 geo-before-auth concern — geo lookup now runs after routing and is skipped for UNAUTHORIZED outcomes (main.rs:145-156; though see the new 🔧 finding above on the gate's overreach)
CI Status
- fmt: PASS (locally)
- clippy: PASS (locally)
- rust tests: PASS (locally, 1016 tests)
- js tests: PASS (CI)
- integration tests: PASS (CI)
- browser integration tests: PASS (CI)
Blocking fixes: - Restore drop(streaming_body) on error path so client sees EOF/truncated response instead of a silently completed partial response (cd4c621 regression) - Add HandlerOutcome::AuthChallenge variant to distinguish our enforce_basic_auth 401s from origin-forwarded 401s; gate geo lookup on AuthChallenge only so origin-forwarded 401s still carry geo headers Non-blocking fixes: - Fix stream_publisher_body doc: remove incorrect async claim; function is sync - Clarify to_fastly_request_ref doc: non-empty body is a caller error - Add enforce_max_body_size helper in http_util; replace 6 duplicated body-size cap blocks across proxy, auction, and request_signing endpoints - Add test for to_fastly_response_skeleton covering status + header round-trip - Restore "bytes" unit in enforce_max_body_size error message for log clarity - Widen enforce_max_body_size what param from &'static str to &str
Summary
httprequest/response types so core handlers no longer depend on Fastly request/response APIs.Changes
crates/trusted-server-adapter-fastly/src/main.rscrates/trusted-server-core/src/auction/endpoints.rs/auctionhandler tohttptypes and rebuild a Fastly request only at the provider-facingAuctionContextboundary.crates/trusted-server-core/src/auction/formats.rscrates/trusted-server-core/src/auction/orchestrator.rscrates/trusted-server-core/src/compat.rscrates/trusted-server-core/src/geo.rscrates/trusted-server-core/src/integrations/google_tag_manager.rscrates/trusted-server-core/src/integrations/gpt.rscrates/trusted-server-core/src/integrations/testlight.rscrates/trusted-server-core/src/migration_guards.rscrates/trusted-server-core/src/proxy.rscrates/trusted-server-core/src/publisher.rscrates/trusted-server-core/src/request_signing/endpoints.rsCloses
Closes #493
Test plan
cargo test --workspacecargo clippy --workspace --all-targets --all-features -- -D warningscargo fmt --all -- --checkcd crates/js/lib && npx vitest run(not run; no JS/TS files changed)cd crates/js/lib && npm run formatcd docs && npm run formatcargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1fastly compute serveChecklist
unwrap()in production code — useexpect("should ...")tracingmacros (notprintln!)