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)
aram356
left a comment
There was a problem hiding this comment.
Summary
Round 3 review. The PR cleanly addresses every blocking finding from the prior two rounds: /verify-signature and /auction now enforce body-size caps (4 KiB and 64 KiB), platform_response_to_fastly was deleted from auction/orchestrator.rs (both call sites use crate::compat::to_fastly_response directly), and /admin/keys/{rotate,deactivate} got matching 4 KiB caps. The expect() in finalize_response (main.rs:251-253) is now safe because Settings::prepare_runtime validates response_headers at startup (settings.rs:486-498).
One new blocking finding: to_fastly_request_ref (introduced in PR 12) silently collapses multi-value headers, which is a latent correctness regression in the provider path. See inline.
CI is green; local cargo test --workspace (814 passed), cargo clippy --workspace --all-targets --all-features -- -D warnings, and cargo fmt --all -- --check all pass.
Blocking
🔧 wrench
to_fastly_request_refclobbers multi-value headers: see inline atcompat.rs:90.
Non-blocking
🤔 thinking
VERIFY_MAX_BODY_BYTES = 4096may be tight for legitimate signed payloads (request_signing/endpoints.rs:78). After base64 signature (~88 bytes for Ed25519),kid, JSON wrapper, and quoting/escaping, the usablepayloadbudget is ~3 KiB. Plausibly fine for typical OpenRTB-shaped payloads but can clash with publishers signing larger blobs (e.g. embedded events). Consider a brief// safety:comment explaining the chosen limit, or making it config-derived alongside the existing GTMmax_beacon_body_sizepattern.
📝 note
- Body-size caps fire after the body is already materialized into memory (
compat.rs:40-43).compat::from_fastly_requestcallsreq.take_body_bytes()at the adapter entry, so the entire body is buffered before any handler-level cap runs. The new caps protect parse allocations but not the receive allocation. This matches the compat shim's semantics today and is a PR-15 concern, not PR-12 — worth pinning in the PR 15 plan that streaming-aware caps need to land with the boundary removal so the protection isn't quietly downgraded when the shim is replaced.
♻️ refactor
-
No 413 regression tests for
/verify-signature,/auction,/admin/keys/{rotate,deactivate}(request_signing/endpoints.rs:93,183,292,auction/endpoints.rs:45). This commit addedsign_rejects_oversized_bodyandrebuild_rejects_oversized_body(proxy.rs:2358-2395) — exactly what the previous review asked for. The same pattern wasn't applied to the four endpoints whose caps are introduced in this commit. Without negative tests, a future commit that drops any cap won't be caught. Same shape as the existing proxy tests — five lines per endpoint. -
Body-size-cap pattern duplicated 6× across handlers.
if body.len() > MAX { return Err(Report::new(RequestTooLarge { format!("X payload {} exceeds limit of {}", body.len(), MAX) })); }appears atproxy.rs:884,proxy.rs:1007,auction/endpoints.rs:45, and three times inrequest_signing/endpoints.rs(93, 183, 292). A small helper (fn enforce_max_body_size(bytes: &[u8], limit: usize, what: &'static str) -> Result<(), Report<TrustedServerError>>) inhttp_utilwould drop ~50 lines and centralize the message format. Defer if you'd rather land in a follow-up.
CI Status
- browser integration tests: PASS
- integration tests: PASS
- prepare integration artifacts: PASS
…feature/edgezero-pr12-handler-layer-types
…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.
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!)