Skip to content

Commit cbce8b0

Browse files
authored
Migrate handler layer to HTTP types (PR 12) (#624) and Migrate integration and provider HTTP types (PR 13) (#626)
* Rename crates to trusted-server-core and trusted-server-adapter-fastly 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. * Add platform abstraction layer with traits and RuntimeServices 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. * Address platform layer review feedback - 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() * Reject host strings containing control characters in BackendConfig * Fix clippy error * Validate scheme and host for control characters in BackendConfig * Address review findings on platform abstraction layer * Address review findings on platform abstraction layer * Add config store read path and storage module split - 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) * Harden legacy config-store reads and align Fastly adapter stubs * Address storage review feedback * Resolved github-advanced-security bot problems * Address PR review feedback on platform abstraction layer - 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) * Add PR 4 design spec for secret store trait (read-only) * Clarify test scope and deferred branches in PR 4 spec * Add implementation plan for PR 4 secret store trait * Add test for get_secret_bytes open-failure path * Add NotImplemented tests for FastlyPlatformSecretStore write stubs * Inline StoreId binding and add section comment in write-stub tests * Remove plan * Add PR 6 design spec for backend and HTTP client traits * Address spec review findings on PR 6 design * Implement PlatformHttpClient and thread RuntimeServices through proxy layer - Add PlatformHttpClient trait with send(), send_async(), and select() methods - Add PlatformBackend trait with predict_name() and ensure() methods - Add PlatformResponse wrapper around EdgeZero HTTP responses - Add PlatformPendingRequest and PlatformSelectResult for auction fan-out - Thread RuntimeServices through IntegrationProxy::handle(), IntegrationRegistry::handle_proxy(), and all first-party proxy endpoints so handlers can reach the HTTP client - Add StubHttpClient and StubBackend test stubs with build_services_with_http_client helper - Add proxy_request_calls_platform_http_client_send integration test - Fix proxy_with_redirects to stay within 7-arg clippy limit via ProxyRequestHeaders struct - Document Body::Stream limitation in edge_request_to_fastly with warning log - Document intentional duplication of platform_response_to_fastly across proxy and orchestrator - Remove spec file (promoted to plan + implementation) * Address pr review findings * Resolve pr review findings * Add PR7 design spec for geo lookup + client info extract-once Documents the call site migration plan: five Fastly SDK extraction points in trusted-server-core replaced by RuntimeServices::client_info reads, following Phase 1 injection pattern from the EdgeZero migration design. * Fix spec review issues in PR7 design doc - Correct erroneous claim about generate_synthetic_id being called twice via DeviceInfo; it is called once (line 91 for fresh_id), DeviceInfo.ip is a separate req.get_client_ip_addr() call fixed independently - Add before/after snippet for handle_publisher_request call site in main.rs - Add noop_services import instruction for http_util.rs test module - Clarify _services rename (drop underscore, not add new param) in didomi.rs - Clarify nextjs #[allow(deprecated)] annotations are out of scope (different function) * Update PR7 spec to address all five agent review findings - Change RequestInfo::from_request signature to &ClientInfo (not &RuntimeServices) so prebid can call it with context.client_info - Scope SDK-call acceptance criteria to active non-deprecated code only - List all six AuctionContext construction sites including two production sites in orchestrator.rs and three test helpers in orchestrator/prebid - Add explicit warn-and-continue pattern for publisher.rs geo lookup - Correct testing table: formats.rs and endpoints.rs have no test modules; add orchestrator.rs and prebid.rs test helper update rows * Add PR7 implementation plan and address plan review findings Plan covers 6 tasks in compilation-safe order: AuctionContext struct change first, then from_request signature, then synthetic.rs cascade, then publisher geo, then didomi. Includes two new copy_headers unit tests (Some/None). Spec fixes: clarify injection pattern exceptions for &ClientInfo and Option<IpAddr>; reword acceptance criterion to reflect that provider-layer reads flow through AuctionContext.client_info. * Fix three plan review findings and two open questions - Finding 1 (High): Add missing publisher.rs test call site at line ~695 for get_or_generate_synthetic_id — was omitted from Task 3 Step 6 - Finding 2 (Medium): Remove crate::geo::GeoInfo import from endpoints.rs rather than replacing it — type is not used by name after the change, keeping any import fails clippy -D warnings - Finding 3 (Low): Replace interactive git add -p in Task 6 with explicit file staging instruction - Open Q1: Add Task 2 step to update stale handle_publisher_request signature in auction/README.md - Open Q2: Add Task 2 step to update from_request doc comment to reflect ClientInfo-based TLS detection instead of Fastly SDK calls * Broaden two low-severity doc cleanup steps in PR7 plan - Step 7: cover all four stale Fastly-SDK-specific locations in http_util.rs (SPOOFABLE_FORWARDED_HEADERS doc, RequestInfo struct doc, from_request doc, detect_request_scheme doc) - Step 8: replace the whole routing snippet in auction/README.md, not just the one handle_publisher_request line — handle_auction and integration_registry.handle_proxy are also stale in that snippet * Fix two remaining low findings in PR7 plan - Add missing Location 2 (RequestInfo.scheme field doc, line ~67) to Step 7; renumber subsequent locations 3-5 - Replace &runtime_services with runtime_services in Step 5 and README snippet — runtime_services is already &RuntimeServices in route_request * Fix count drift in Step 7: four → five locations * Add client_info field to AuctionContext and fix all construction sites * Change RequestInfo::from_request to take &ClientInfo, thread services into handle_publisher_request * Add Task 2 follow-up coverage and README route fixes * Add services param to generate_synthetic_id, remove Fastly IP/geo calls in formats and endpoints * Revert premature publisher geo change from Task 3 * Replace deprecated GeoInfo::from_request in publisher.rs with services.geo().lookup() * Remove Fastly IP extraction from Didomi copy_headers, use ClientInfo instead * Move IpAddr import to test module level in didomi.rs * Apply rustfmt formatting to didomi.rs, publisher.rs, and synthetic.rs Fix multi-line function call style in didomi.rs, line-break wrapping in publisher.rs test, and import ordering in synthetic.rs test module. * Add test coverage for generate_synthetic_id with concrete client IP Adds noop_services_with_client_ip helper to test_support and a new test that verifies the client_ip path through generate_synthetic_id by asserting the HMAC differs when the IP changes. * Align geo lookup warn log format with codebase convention ({e} not {e:?}) * Apply Prettier formatting to PR7 plan and spec docs * Document content rewriting as platform-agnostic in platform module * Document html_processor as platform-agnostic * Document streaming_processor as platform-agnostic * Fix unresolved doc link: replace EdgeRequest with edgezero_core::http::Request * Add plan for content rewriting * Add plan for PR9: wire signing to store primitives * Add build_services_with_config_and_secret to test_support * Add FastlyManagementApiClient to adapter * Implement FastlyPlatformConfigStore and FastlyPlatformSecretStore write methods via management API Replace FastlyApiClient with FastlyManagementApiClient in the put/delete methods of FastlyPlatformConfigStore and the create/delete methods of FastlyPlatformSecretStore. Remove the now-unused FastlyApiClient import. * Migrate KeyRotationManager from FastlyApiClient to RuntimeServices store primitives * Migrate signing.rs from FastlyConfigStore/FastlySecretStore to RuntimeServices * Delete storage/api_client.rs from core; remove FastlyApiClient * Fix formatting after CI gate check * Add services to AuctionContext; remove deprecated from_config shim Thread RuntimeServices into AuctionContext so auction providers can access platform stores directly. Update PrebidAuctionProvider to use RequestSigner::from_services(context.services) instead of the now- removed from_config() shim. All construction sites and test helpers updated accordingly. This satisfies the final acceptance criterion of #490: no FastlyConfigStore/FastlySecretStore construction remains in the request_signing/ modules. * Fix prettier formatting in PR9 plan document * Add PR 10 logging initialization design * Add PR 10 logging initialization plan * Fix PR 10 logging plan to avoid per-log allocation * Extract Fastly logging initialization into adapter module * Wire Fastly main.rs to adapter-local logging module * Remove log-fastly from core dependencies * Format Fastly logging module declaration * format plan docs * Address PR findings * Restore idiomatic fern logging and improve target label extraction - Reverted gratuitous _message rename and record.args() usage in logging.rs, returning to the idiomatic message parameter inside the fern format closure. - Refactored target_label to use .rsplit_once("::") rather than .split("::").last(). This provides a more explicit and robust way to extract the final module segment. - Expanded target_label test coverage to explicitly test edge cases such as inputs without :: separators, empty strings, and inputs with trailing ::. * Migrate utility layer to HTTP types * Migrate handler layer to HTTP types * Address PR review findings * Address review findings * Address review findings * Resolve review findings * Resolve PR review findings * Address review findings * Removed unused import * Fix rotate/delete atomicity, HTTP verb, idempotent deletes, and weak tests * Resolve PR review feedback on logging module * Address review findings * Resolve PR review findings * Address round-3 review findings - Revert proxy.rs merge artifact: restore per-request allowed_domains at both redirect_is_permitted call sites; remove dead_code allow and stale comment — integration proxies defaulting to &[] get open mode again as documented - Drop unused trusted-server-js dep from adapter Cargo.toml - Fix check_response: gate body read behind error branch so 2xx paths do not buffer and discard the response body - Remove self-referential SECRET_UPSERT_METHOD test - Reorder write-cost doc so outbound HTTPS round-trip leads; handle-open caching noted as negligible - Refactor make_request to take fastly::http::Method; drop string match and unreachable arm; remove SECRET_UPSERT_METHOD const - Add SigningStoreIds named struct in endpoints.rs; update both call sites to destructure by name * Address PR review: add body-size caps and remove orchestrator duplication - Add VERIFY_MAX_BODY_BYTES (4096) cap to handle_verify_signature - Add AUCTION_MAX_BODY_BYTES (65536) cap to handle_auction - Add ADMIN_MAX_BODY_BYTES (4096) cap to handle_rotate_key and handle_deactivate_key - Delete platform_response_to_fastly from orchestrator; both call sites now use crate::compat::to_fastly_response directly - Add sign_rejects_oversized_body and rebuild_rejects_oversized_body regression tests asserting 413 on oversized POST bodies * Resolve PR review findings * Resolve PR review findings * Resolve PR review findings * Use append_header in place of set_header * fix lint * Route Fastly cookie calls through compat bridge after PR10 merge 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). * Remove unused Logger import * Resolve PR review findings * Fix fmt lint * Rename synthetic_id_cookie_value_is_safe → ec_cookie_value_is_safe * Resolve PR review findings * Resolve PR 624 review findings 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 * Call StreamingBody::finish() to properly terminate chunked response 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. * Apply cargo fmt formatting * Resolve PR 624 round-6 review findings 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 * Bound publisher response body size and document interim buffering Add MAX_PLATFORM_RESPONSE_BODY_BYTES (10 MiB) cap in fastly_response_to_platform to prevent heap exhaustion from large origin responses. Content-Length pre-check avoids the WASM heap copy entirely for declared-size responses; post-materialization check covers chunked responses without Content-Length. Document the interim buffering regression in PublisherResponse::Stream and PassThrough doc comments, noting that both variants now hold a fully materialised body until the platform HTTP client gains streaming response support in PR 15. Resolves PR 624 round-7 review findings. * Add edge_cookie module, test helpers, and integration handle signatures Complete merge resolution of remaining unstaged files: - lib.rs: expose edge_cookie as pub(crate) module (used by proxy.rs) - platform/test_support.rs: add recorded_request_headers() on StubHttpClient and build_services_with_http_client() factory for tests with custom HTTP clients - integrations/{datadome,didomi,lockr,permutive,prebid,sourcepoint}: add _services: &RuntimeServices parameter to handle() following main branch IntegrationProxy trait change * Fix remaining code review findings from merge Address medium-priority findings surfaced during review of the main merge: - test_support.rs: record request headers in send_async (was only recorded in send), so recorded_request_headers() is consistent across sync and async HTTP test paths - test_support.rs: document StubBackend vs NoopBackend distinction on build_services_with_http_client so callers understand the tradeoff - endpoints.rs: 413 PAYLOAD_TOO_LARGE response now includes Content-Type and a client-facing message instead of an empty body with a server-internal error string - kv.rs: derive Clone for KvIdentityGraph (trivial String wrapper) - main.rs: build KvIdentityGraph once and clone for finalize_kv_graph, eliminating the double call to maybe_identity_graph * Fix cargo fmt lint * Migrate auction format tests to edgezero http types after merge Merge 6e191e9 combined the edgezero http::Request/Response migration with new auction tests from main that still used the Fastly SDK API, and added the RequestTooLarge error variant. Update the auction format tests to build Request<EdgeBody> via the http builder, read response bodies through EdgeBody::into_bytes, pass the new services and geo arguments to convert_tsjs_to_auction_request, and use http accessors. Cover the new RequestTooLarge variant in the error status-code guard and mapping test. * Migrate integration and provider HTTP types (PR 13) (#626) * Migrate integration and provider HTTP types * Resolve PR findings * Resolve PR findings * Address review findings: safe body reads and bounded inbound forwarding - Add collect_body_bounded helper with INTEGRATION_MAX_BODY_BYTES (256 KiB) cap to prevent unbounded memory use on streaming bodies - Replace all into_bytes() calls (panic on Body::Stream) with collect_body or collect_body_bounded across integrations and auction endpoint - Make AuctionProvider::parse_response async so implementations can safely drain response bodies without panicking on the Stream variant - Add .await to both parse_response call sites in the orchestrator - Cap inbound request bodies in lockr, permutive, datadome, and didomi proxy handlers using collect_body_bounded before forwarding upstream - Fix lockr Origin header: replace expect() with a warn-and-skip fallback so an invalid user-supplied origin cannot crash the edge worker - Add PayloadSizeError::StreamRead variant to google_tag_manager and return 502 (not 413) when a stream transport error occurs while reading the body - Remove extra blank line before closing brace in google_tag_manager impl block * Address PR review: fmt, testlight body cap, auction constant, stale README - Run cargo fmt --all (7+ files had formatting failures) - Cap testlight POST body with collect_body_bounded + INTEGRATION_MAX_BODY_BYTES - Use dedicated AUCTION_MAX_BODY_BYTES (256 KiB) for /auction instead of INTEGRATION_MAX_BODY_BYTES - Update auction/README.md: parse_response is now async, parallel execution via select() is live * Resolve PR review findings * Resolve PR 626 review findings Blocking fixes: - Fix doc_markdown clippy errors in integrations/mod.rs (BAD_GATEWAY, max_bytes, one_chunk wrapped in backticks) - Drop change_context() on collect_body_bounded() calls in auction, datadome, didomi, lockr, permutive — RequestTooLarge (413) was being rewritten to Integration/Auction (502); now propagates unchanged via ? so oversized bodies correctly surface as PAYLOAD_TOO_LARGE - Remove debug_assert! in compat::to_fastly_response (already fixed in PR 12, missed in PR 13 merge) Non-blocking fixes: - Migrate testlight upstream response read from unbounded collect_body to collect_response_bounded(UPSTREAM_RTB_MAX_RESPONSE_BYTES) matching all other RTB integrations; remove now-unused collect_body function - Upgrade orchestrator predicted-backend-name fallback log from debug to warn so operators see silent bid-loss fallback in production * Apply cargo fmt formatting * Resolve PR 626 round-1 review findings Blocking fixes: - Use collect_response_bounded for sourcepoint JS response bodies, replacing the EdgeBody::Stream branch that silently returned an empty body; streaming responses now drain up to the 5 MiB cap and surface oversized bodies as Integration/502, matching all other integrations - Run cargo fmt to fix 8 formatting hunks in sourcepoint.rs introduced by the May 12 merge Non-blocking: - Orchestrator now identifies the failing provider on select() Err by diffing backend_to_provider keys against the new remaining list; logs with provider name and pushes AuctionResponse::error so the response array counts correctly Refactor: - Add first_byte_timeout: Option<Duration> to ensure_integration_backend; proxy call sites pass None (keeps 15 s default); auction providers (aps, adserver_mock, prebid) switch from BackendConfig::from_url_with_first_byte_timeout to ensure_integration_backend with the auction-scoped timeout, going through the platform abstraction consistently * Resolve PR 626 round-2 review findings - Fix orchestrator select() Err path: add failed_backend_name to PlatformSelectResult and populate it from fastly::SendError::backend_name() in the Fastly adapter; orchestrator now uses this field directly instead of the broken diff-against-remaining logic (which always failed on Fastly because remaining entries have no backend names) - Add push_select_error() to StubHttpClient and strip backend names from remaining entries in select() to match Fastly production behavior - Add select_error_is_attributed_to_correct_provider test: two stub providers, one fails via injected select error, assert correct attribution - Remove CONTENT_LENGTH from datadome headers_to_copy (body is re-materialized so client Content-Length is wrong for the outbound request) - Replace _request_info ignored param in prebid to_openrtb with request_info and remove the duplicate internal RequestInfo::from_request call - Replace from_utf8_lossy with strict from_utf8 + fallback in google_tag_manager - Replace .expect() on HeaderValue::from_str with if let Ok in adserver_mock * Give auction transport failures a consistent error envelope Provider transport failures (select() errors) pushed a bare AuctionResponse::error with no metadata, while parse and launch failures attach error_type/message. Route transport failures through a shared helper with a new ERROR_TYPE_TRANSPORT classifier and a static, upstream-safe message, so the public /auction response shape no longer depends on how a provider failed. Add unit coverage for the new helper.
1 parent bc6d78c commit cbce8b0

40 files changed

Lines changed: 5357 additions & 2655 deletions

crates/trusted-server-adapter-fastly/src/main.rs

Lines changed: 393 additions & 232 deletions
Large diffs are not rendered by default.

crates/trusted-server-adapter-fastly/src/platform.rs

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,15 @@ fn edge_request_to_fastly(
324324
Ok(fastly_req)
325325
}
326326

327+
/// Maximum origin response body size copied into WASM heap.
328+
///
329+
/// `take_body_bytes()` copies the full origin response into a single
330+
/// allocation. This cap prevents oversized origin responses from exhausting
331+
/// the WASM address space. The Content-Length pre-check avoids the copy
332+
/// entirely for responses that declare their size. Streaming response support
333+
/// will remove this limit in PR 15.
334+
const MAX_PLATFORM_RESPONSE_BODY_BYTES: usize = 10 * 1024 * 1024; // 10 MiB
335+
327336
fn fastly_body_to_edge_stream(body: fastly::Body) -> edgezero_core::body::Body {
328337
let stream = futures::stream::unfold(Some(body), |state| async move {
329338
let mut body = state?;
@@ -347,6 +356,24 @@ fn fastly_response_to_platform(
347356
backend_name: impl Into<String>,
348357
stream_response: bool,
349358
) -> Result<PlatformResponse, Report<PlatformError>> {
359+
// Pre-flight: reject oversized responses before copying bytes into WASM heap.
360+
// Content-Length is advisory but covers most origin responses; chunked
361+
// responses without it fall through to the post-materialization check below.
362+
if !stream_response {
363+
if let Some(claimed_len) = resp
364+
.get_header("content-length")
365+
.and_then(|v| v.to_str().ok())
366+
.and_then(|s| s.trim().parse::<usize>().ok())
367+
{
368+
if claimed_len > MAX_PLATFORM_RESPONSE_BODY_BYTES {
369+
return Err(Report::new(PlatformError::HttpClient).attach(format!(
370+
"origin Content-Length {claimed_len} exceeds \
371+
{MAX_PLATFORM_RESPONSE_BODY_BYTES}-byte response body limit"
372+
)));
373+
}
374+
}
375+
}
376+
350377
let status = resp.get_status();
351378
let mut builder = edgezero_core::http::response_builder().status(status);
352379
for (name, value) in resp.get_headers() {
@@ -355,7 +382,18 @@ fn fastly_response_to_platform(
355382
let body = if stream_response {
356383
fastly_body_to_edge_stream(resp.take_body())
357384
} else {
358-
edgezero_core::body::Body::from(resp.take_body_bytes())
385+
let body_bytes = resp.take_body_bytes();
386+
387+
// Belt-and-suspenders: catches chunked responses without Content-Length.
388+
if body_bytes.len() > MAX_PLATFORM_RESPONSE_BODY_BYTES {
389+
return Err(Report::new(PlatformError::HttpClient).attach(format!(
390+
"origin response body {} bytes exceeds \
391+
{MAX_PLATFORM_RESPONSE_BODY_BYTES}-byte limit",
392+
body_bytes.len()
393+
)));
394+
}
395+
396+
edgezero_core::body::Body::from(body_bytes)
359397
};
360398
let edge_response = builder
361399
.body(body)
@@ -453,7 +491,7 @@ impl PlatformHttpClient for FastlyPlatformHttpClient {
453491
.map(PlatformPendingRequest::new)
454492
.collect();
455493

456-
let ready = match result {
494+
let (ready, failed_backend_name) = match result {
457495
Ok(fastly_resp) => {
458496
let backend_name = fastly_resp
459497
.get_backend_name()
@@ -462,15 +500,27 @@ impl PlatformHttpClient for FastlyPlatformHttpClient {
462500
""
463501
})
464502
.to_string();
465-
fastly_response_to_platform(fastly_resp, backend_name, false)
503+
(
504+
fastly_response_to_platform(fastly_resp, backend_name, false),
505+
None,
506+
)
466507
}
467508
Err(e) => {
468-
Err(Report::new(PlatformError::HttpClient)
469-
.attach(format!("fastly select error: {e}")))
509+
let failed_name = e.backend_name().to_string();
510+
(
511+
Err(Report::new(PlatformError::HttpClient).attach(format!(
512+
"fastly select error for backend '{failed_name}': {e}"
513+
))),
514+
Some(failed_name),
515+
)
470516
}
471517
};
472518

473-
Ok(PlatformSelectResult { ready, remaining })
519+
Ok(PlatformSelectResult {
520+
ready,
521+
remaining,
522+
failed_backend_name,
523+
})
474524
}
475525
}
476526

crates/trusted-server-adapter-fastly/src/route_tests.rs

Lines changed: 101 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ use edgezero_core::body::Body as EdgeBody;
77
use edgezero_core::http::response_builder as edge_response_builder;
88
use edgezero_core::key_value_store::NoopKvStore;
99
use error_stack::Report;
10-
use fastly::http::request::PendingRequest;
1110
use fastly::http::{header, Method, StatusCode};
1211
use fastly::Request;
1312
use serde_json::json;
1413
use trusted_server_core::auction::{
1514
build_orchestrator, AuctionContext, AuctionOrchestrator, AuctionProvider, AuctionRequest,
1615
AuctionResponse,
1716
};
17+
use trusted_server_core::compat;
18+
use trusted_server_core::ec::finalize::ec_finalize_response;
1819
use trusted_server_core::ec::registry::PartnerRegistry;
1920
use trusted_server_core::error::TrustedServerError;
2021
use trusted_server_core::integrations::IntegrationRegistry;
@@ -24,13 +25,14 @@ use trusted_server_core::platform::{
2425
PlatformResponse, PlatformSecretStore, PlatformSelectResult, RuntimeServices, StoreId,
2526
StoreName,
2627
};
28+
use trusted_server_core::proxy::AssetProxyCachePolicy;
2729
use trusted_server_core::request_signing::JWKS_CONFIG_STORE_NAME;
2830
use trusted_server_core::settings::{
2931
AssetImageOptimizerConfig, AssetOriginAuth, ImageOptimizerProfileSet, ImageOptimizerSettings,
3032
ProxyAssetRoute, S3SigV4AuthConfig, Settings,
3133
};
3234

33-
use super::route_request;
35+
use super::{route_request, HandlerOutcome};
3436

3537
struct StubJwksConfigStore;
3638

@@ -310,24 +312,25 @@ impl PlatformGeo for NoopGeo {
310312

311313
struct DisabledRouteProvider;
312314

315+
#[async_trait::async_trait(?Send)]
313316
impl AuctionProvider for DisabledRouteProvider {
314317
fn provider_name(&self) -> &'static str {
315318
"disabled-route"
316319
}
317320

318-
fn request_bids(
321+
async fn request_bids(
319322
&self,
320323
_request: &AuctionRequest,
321324
_context: &AuctionContext<'_>,
322-
) -> Result<PendingRequest, Report<TrustedServerError>> {
325+
) -> Result<PlatformPendingRequest, Report<TrustedServerError>> {
323326
Err(Report::new(TrustedServerError::Auction {
324327
message: "disabled route provider should not launch requests".to_string(),
325328
}))
326329
}
327330

328-
fn parse_response(
331+
async fn parse_response(
329332
&self,
330-
_response: fastly::Response,
333+
_response: PlatformResponse,
331334
_response_time_ms: u64,
332335
) -> Result<AuctionResponse, Report<TrustedServerError>> {
333336
Err(Report::new(TrustedServerError::Auction {
@@ -512,6 +515,58 @@ fn test_partner_registry(settings: &Settings) -> PartnerRegistry {
512515
PartnerRegistry::from_config(&settings.ec.partners).expect("should build partner registry")
513516
}
514517

518+
fn route_result_to_fastly_response(
519+
settings: &Settings,
520+
services: &RuntimeServices,
521+
partner_registry: &PartnerRegistry,
522+
route_result: super::RouteResult,
523+
) -> fastly::Response {
524+
let super::RouteResult {
525+
outcome,
526+
ec_context,
527+
finalize_kv_graph,
528+
eids_cookie,
529+
sharedid_cookie,
530+
should_finalize_ec,
531+
asset_cache_policy,
532+
..
533+
} = route_result;
534+
535+
let is_auth_challenge = matches!(&outcome, HandlerOutcome::AuthChallenge(_));
536+
let mut response = match outcome {
537+
HandlerOutcome::Buffered(response) | HandlerOutcome::AuthChallenge(response) => {
538+
Some(response)
539+
}
540+
_ => None,
541+
}
542+
.expect("should have a buffered route response");
543+
544+
let geo_info = if is_auth_challenge {
545+
None
546+
} else {
547+
services
548+
.geo()
549+
.lookup(services.client_info().client_ip)
550+
.unwrap_or(None)
551+
};
552+
super::finalize_response(settings, geo_info.as_ref(), &mut response);
553+
asset_cache_policy.apply_after_route_finalization(&mut response);
554+
555+
let mut fastly_response = compat::to_fastly_response(response);
556+
if should_finalize_ec {
557+
ec_finalize_response(
558+
settings,
559+
&ec_context,
560+
finalize_kv_graph.as_ref(),
561+
partner_registry,
562+
eids_cookie.as_deref(),
563+
sharedid_cookie.as_deref(),
564+
&mut fastly_response,
565+
);
566+
}
567+
fastly_response
568+
}
569+
515570
fn route_auction(settings: &Settings, body: impl Into<Vec<u8>>) -> fastly::Response {
516571
let (orchestrator, integration_registry) = build_route_stack(settings);
517572

@@ -531,17 +586,16 @@ fn route_auction_with_stack(
531586
.with_body(body.into());
532587
let services = test_runtime_services(&req);
533588

534-
futures::executor::block_on(route_request(
589+
let route_result = futures::executor::block_on(route_request(
535590
settings,
536591
orchestrator,
537592
integration_registry,
538593
&partner_registry,
539594
&services,
540-
req,
595+
compat::from_fastly_request(req),
541596
))
542-
.expect("should route auction request")
543-
.response
544-
.expect("should buffer auction response in tests")
597+
.expect("should route auction request");
598+
route_result_to_fastly_response(settings, &services, &partner_registry, route_result)
545599
}
546600

547601
fn route_buffered_response(
@@ -555,17 +609,16 @@ fn route_buffered_response(
555609
let partner_registry =
556610
PartnerRegistry::from_config(&settings.ec.partners).expect("should build partner registry");
557611

558-
futures::executor::block_on(route_request(
612+
let route_result = futures::executor::block_on(route_request(
559613
settings,
560614
orchestrator,
561615
integration_registry,
562616
&partner_registry,
563617
services,
564-
req,
618+
compat::from_fastly_request(req),
565619
))
566-
.expect(expect_message)
567-
.response
568-
.expect("should buffer route response in tests")
620+
.expect(expect_message);
621+
route_result_to_fastly_response(settings, services, &partner_registry, route_result)
569622
}
570623

571624
fn valid_banner_ad_unit_body() -> Vec<u8> {
@@ -592,42 +645,36 @@ fn routes_use_request_local_consent() {
592645
IntegrationRegistry::new(&settings).expect("should create integration registry");
593646
let partner_registry = test_partner_registry(&settings);
594647

595-
let discovery_req = Request::get("https://test.com/.well-known/trusted-server.json");
596-
let discovery_services = test_runtime_services(&discovery_req);
648+
let discovery_fastly_req = Request::get("https://test.com/.well-known/trusted-server.json");
649+
let discovery_services = test_runtime_services(&discovery_fastly_req);
597650
let discovery_resp = futures::executor::block_on(route_request(
598651
&settings,
599652
&orchestrator,
600653
&integration_registry,
601654
&partner_registry,
602655
&discovery_services,
603-
discovery_req,
656+
compat::from_fastly_request(discovery_fastly_req),
604657
))
605658
.expect("should route discovery request");
606-
let discovery_response = discovery_resp
607-
.response
608-
.expect("should buffer discovery response in tests");
609659
assert_eq!(
610-
discovery_response.get_status(),
660+
discovery_resp.outcome.status(),
611661
StatusCode::OK,
612662
"should keep discovery available with request-local consent"
613663
);
614664

615-
let admin_req = Request::post("https://test.com/_ts/admin/keys/rotate");
616-
let admin_services = test_runtime_services(&admin_req);
665+
let admin_fastly_req = Request::post("https://test.com/_ts/admin/keys/rotate");
666+
let admin_services = test_runtime_services(&admin_fastly_req);
617667
let admin_resp = futures::executor::block_on(route_request(
618668
&settings,
619669
&orchestrator,
620670
&integration_registry,
621671
&partner_registry,
622672
&admin_services,
623-
admin_req,
673+
compat::from_fastly_request(admin_fastly_req),
624674
))
625675
.expect("should route admin request");
626-
let admin_response = admin_resp
627-
.response
628-
.expect("should buffer admin response in tests");
629676
assert_eq!(
630-
admin_response.get_status(),
677+
admin_resp.outcome.status(),
631678
StatusCode::UNAUTHORIZED,
632679
"should keep admin auth behavior unchanged with request-local consent"
633680
);
@@ -822,17 +869,18 @@ fn asset_routes_stream_asset_responses_directly() {
822869
IntegrationRegistry::new(&settings).expect("should create integration registry");
823870
let partner_registry = test_partner_registry(&settings);
824871

825-
let mut req = Request::get("https://test.com/.images/logo.png");
826-
req.set_header(header::COOKIE, format!("ts-ec={}", valid_ec_id()));
827-
req.set_header("sec-gpc", "1");
872+
let mut fastly_req = Request::get("https://test.com/.images/logo.png");
873+
fastly_req.set_header(header::COOKIE, format!("ts-ec={}", valid_ec_id()));
874+
fastly_req.set_header("sec-gpc", "1");
828875
let http_client = Arc::new(StreamingRecordingHttpClient::new());
829876
let services = test_runtime_services_with_secret_http_client_and_geo(
830-
&req,
877+
&fastly_req,
831878
Arc::new(FixedBackend),
832879
Arc::new(NoopSecretStore),
833880
Arc::clone(&http_client) as Arc<dyn PlatformHttpClient>,
834881
Arc::new(FixedGeo(us_california_geo())),
835882
);
883+
let req = compat::from_fastly_request(fastly_req);
836884

837885
let outcome = futures::executor::block_on(route_request(
838886
&settings,
@@ -845,12 +893,27 @@ fn asset_routes_stream_asset_responses_directly() {
845893
.expect("should route streaming asset request");
846894

847895
assert!(
848-
outcome.response.is_none(),
849-
"streaming asset route should send directly instead of returning a buffered response"
896+
!outcome.should_finalize_ec,
897+
"asset routes should not emit EC identity headers"
898+
);
899+
assert_eq!(
900+
outcome.asset_cache_policy,
901+
AssetProxyCachePolicy::OriginControlled,
902+
"successful asset routes should preserve origin cache policy"
903+
);
904+
let (response, body) = match outcome.outcome {
905+
HandlerOutcome::AssetStreaming { response, body } => Some((response, body)),
906+
_ => None,
907+
}
908+
.expect("should return streaming asset outcome");
909+
assert_eq!(
910+
response.status(),
911+
StatusCode::OK,
912+
"should preserve streaming asset response status"
850913
);
851914
assert!(
852-
outcome.pull_sync_context.is_none(),
853-
"asset routes should not schedule publisher pull-sync work"
915+
matches!(body, EdgeBody::Stream(_)),
916+
"should preserve streaming asset response body"
854917
);
855918
let calls = http_client
856919
.calls

0 commit comments

Comments
 (0)