Skip to content

Migrate integration and provider HTTP types (PR 13)#626

Open
prk-Jr wants to merge 11 commits intofeature/edgezero-pr12-handler-layer-typesfrom
feature/edgezero-pr13-integration-provider-type-migration
Open

Migrate integration and provider HTTP types (PR 13)#626
prk-Jr wants to merge 11 commits intofeature/edgezero-pr12-handler-layer-typesfrom
feature/edgezero-pr13-integration-provider-type-migration

Conversation

@prk-Jr
Copy link
Copy Markdown
Collaborator

@prk-Jr prk-Jr commented Apr 9, 2026

Summary

  • Replace the remaining Fastly request and response boundaries in the integration and auction provider layers so PR13 runs on platform-agnostic HTTP types.
  • Move provider dispatch onto RuntimeServices HTTP client primitives with async request_bids, PlatformPendingRequest, and PlatformResponse while preserving orchestrator deadline handling.
  • Finish the review hardening for Testlight response normalization and convert the migrated integration tests to HTTP-native fixtures.

Changes

File Change
crates/trusted-server-adapter-fastly/src/main.rs Route integration proxy requests through the HTTP-native registry boundary and pass RuntimeServices into the PR13 entrypoint.
crates/trusted-server-core/src/auction/README.md Update provider guidance and examples to PlatformPendingRequest, PlatformResponse, and the platform HTTP client flow.
crates/trusted-server-core/src/auction/endpoints.rs Thread RuntimeServices into the auction entrypoint context.
crates/trusted-server-core/src/auction/orchestrator.rs Await async provider launches, operate on PlatformPendingRequest/select, and update the remaining PR13 migration comments.
crates/trusted-server-core/src/auction/provider.rs Convert AuctionProvider to async request_bids with PlatformPendingRequest/PlatformResponse and refresh the trait docs.
crates/trusted-server-core/src/auction/types.rs Add RuntimeServices to AuctionContext.
crates/trusted-server-core/src/integrations/adserver_mock.rs Migrate the mock provider to platform HTTP pending/response types.
crates/trusted-server-core/src/integrations/aps.rs Migrate APS provider request dispatch and response parsing to platform HTTP types.
crates/trusted-server-core/src/integrations/datadome.rs Convert DataDome proxying to HTTP-native request/response handling via the platform client.
crates/trusted-server-core/src/integrations/didomi.rs Convert Didomi proxying to HTTP-native request/response handling via the platform client.
crates/trusted-server-core/src/integrations/google_tag_manager.rs Remove Fastly shims, keep bounded POST handling on HTTP bodies, and convert GTM tests to HTTP-native fixtures.
crates/trusted-server-core/src/integrations/gpt.rs Remove Fastly request/response compat from GPT asset proxying and convert GPT tests to HTTP-native fixtures.
crates/trusted-server-core/src/integrations/lockr.rs Convert Lockr SDK/API proxy handling to HTTP-native request/response types.
crates/trusted-server-core/src/integrations/permutive.rs Convert Permutive SDK/API proxy handling to HTTP-native request/response types.
crates/trusted-server-core/src/integrations/prebid.rs Move Prebid provider dispatch to async platform HTTP requests and parse PlatformResponse results.
crates/trusted-server-core/src/integrations/registry.rs Migrate IntegrationProxy/routing types to http types and thread RuntimeServices through proxy dispatch.
crates/trusted-server-core/src/integrations/testlight.rs Convert Testlight to HTTP-native request/response handling, add the stale Content-Length regression test, and drop stale length headers when rebuilding JSON responses.

Closes

Closes #494

Test plan

  • cargo test --workspace
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo fmt --all -- --check
  • JS tests: cd crates/js/lib && npx vitest run
  • JS format: cd crates/js/lib && npm run format
  • Docs format: cd docs && npm run format
  • WASM build: cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1
  • Manual testing via fastly compute serve
  • Other: cargo doc --no-deps --all-features (passes with pre-existing unrelated rustdoc warnings outside this PR's scope)

Checklist

  • Changes follow CLAUDE.md conventions
  • No unwrap() in production code — use expect("should ...")
  • Uses repo-standard logging macros, not println!
  • New code has tests
  • No secrets or credentials committed

@prk-Jr prk-Jr self-assigned this Apr 9, 2026
@prk-Jr prk-Jr changed the title Migrate integration and provider HTTP types Migrate integration and provider HTTP types (PR 13) Apr 9, 2026
@prk-Jr prk-Jr requested review from ChristianPavilonis and aram356 and removed request for aram356 April 9, 2026 12:04
@prk-Jr prk-Jr linked an issue Apr 9, 2026 that may be closed by this pull request
Copy link
Copy Markdown
Collaborator

@ChristianPavilonis ChristianPavilonis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review — Migrate integration and provider HTTP types (PR 13)

1 blocking finding, 5 non-blocking suggestions.

Blocking

🔧 Missing Content-Type forwarding for Didomi POST/PUT — see inline comment.

Non-blocking (inline)

  • 🤔 Prebid still imports from fastly::http — last remaining use fastly in the integration layer
  • ♻️ Duplicated body collection helpers (collect_response_body / collect_body_bytes)
  • 🌱 Unbounded response body collection (matches pre-migration, but could use a size cap)
  • ⛏ Dead "X-" uppercase branch in custom header copy (http crate lowercases names)
  • ⛏ Duplicated backend_name_for_url helper across 4 integrations

Comment thread crates/trusted-server-core/src/integrations/didomi.rs
Comment thread crates/trusted-server-core/src/integrations/prebid.rs Outdated
Comment thread crates/trusted-server-core/src/integrations/google_tag_manager.rs Outdated
Comment thread crates/trusted-server-core/src/integrations/lockr.rs Outdated
Comment thread crates/trusted-server-core/src/integrations/datadome.rs
Copy link
Copy Markdown
Collaborator

@ChristianPavilonis ChristianPavilonis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ignore this

@ChristianPavilonis ChristianPavilonis dismissed their stale review April 14, 2026 19:19

Replacing with full review

Copy link
Copy Markdown
Collaborator

@ChristianPavilonis ChristianPavilonis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Solid migration of the integration and auction provider layers from Fastly-specific HTTP types to platform-agnostic http crate types. No blocking issues found. CI is passing.

Non-blocking

♻️ refactor

  • Duplicated body_as_reader helper: proxy.rs:24 and publisher.rs:23 define identical body_as_reader functions. Consider extracting into a shared location (e.g., an into_reader() method on EdgeBody, or a helper in http_util.rs).

📝 note

  • PR description file table incomplete: proxy.rs and publisher.rs have changes in this diff (the body_as_reader extraction and test type alias cleanups) but are not listed in the PR body's change table.

📌 out of scope

  • Orchestrator tests disabled pending mock PlatformHttpClient: Several provider integration tests and timeout enforcement paths in orchestrator.rs:751-765 are disabled pending a mock for PlatformHttpClient::select(). This is acknowledged in the PR, but the scope of untested paths has grown — a follow-up to add thin mock support for select() would significantly improve coverage of the deadline enforcement logic.

Comment thread crates/trusted-server-core/src/integrations/mod.rs
Comment thread crates/trusted-server-core/src/integrations/google_tag_manager.rs Outdated
Copy link
Copy Markdown
Collaborator

@aram356 aram356 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

The refactor is clean and the ensure_integration_backend / collect_body helpers extracted in the fix commits are a genuine improvement. The main risks are latent panics and missing POST size bounds that were introduced in the base migration commit. None are new in the fix commits, but this PR is the natural owner of the HTTP-type boundary, so they should land before merge.

Blocking

🔧 wrench

  • EdgeBody::into_bytes() panics on streaming bodies at auction/endpoints.rs:42 — the inbound auction POST body is user-controlled. Body::into_bytes() panics on Body::Stream(_) (unit test into_bytes_panics_for_stream asserts this). Today Fastly's adapter happens to return Body::Once, so nothing panics — but that invariant is undocumented and the /auction handler is the first publicly-reachable crash if it ever changes. Fix: switch to Body::into_bytes_bounded(max).await with a documented cap (e.g. 256KB).
  • EdgeBody::into_bytes() panics across every migrated provider/proxy — same root cause, see inline comments on prebid.rs:1157, permutive.rs:151, lockr.rs:149, datadome.rs:301, aps.rs:554, adserver_mock.rs:373. Make parse_response async and read bodies with into_bytes_bounded.
  • Unbounded inbound POST body forwarding on the datadome / didomi / permutive / lockr proxies — only GTM enforces a cap. Promote the GTM size-bounded reader into integrations/mod.rs as collect_body_bounded and apply it uniformly. Inline comments on datadome.rs:367, didomi.rs:245, permutive.rs:211, lockr.rs:203.
  • Lockr .expect() on user-controlled Origin header — reachable worker panic at lockr.rs:266.
  • GTM stream-read errors mis-classified as PayloadTooLarge — returns 413 for transport errors at google_tag_manager.rs:313. Add a StreamRead variant and map to 502.

Non-blocking

🤔 thinking

  • collect_body has no size cap (integrations/mod.rs:101) — silently drains whole bodies to RAM. Same concern applies to the GTM response-rewrite path at google_tag_manager.rs:486.
  • Orchestrator select_result.ready error path drops provider identity in run_providers_parallel — when ready is Err, the provider identity is lost so no AuctionResponse::error(...) is pushed and the failing backend vanishes silently from the result set. Track a (backend_name, provider_index) pair alongside fastly_pending so failures are attributable.
  • Deadline enforcement relies on every provider honoring backend_name(effective_timeout) — Phase 1 computes remaining_ms.min(provider.timeout_ms()) correctly, but nothing guarantees each provider actually threads that timeout through to the backend config. Not a new regression, but the async migration makes it more load-bearing. Add a unit test that asserts the select loop returns before deadline + epsilon.

♻️ refactor

  • aps.rs / adserver_mock.rs still build BackendConfig inline — extend the new ensure_integration_backend helper to cover providers too.

🌱 seedling

  • Make parse_response async in the provider trait (auction/provider.rs:48) — needed anyway once the into_bytes_bounded fix lands; zero cost under #[async_trait(?Send)].

📝 note

  • Testlight stale Content-Length test is narrow (testlight.rs:425) — good unit-level coverage, but the two handle() rebuild call sites would benefit from an end-to-end assertion.

👍 praise

  • Clean helper extraction in integrations/mod.rs — real dedup across six integrations.
  • Dropping the dead || name_str.starts_with("X-") branch in lockr.rs / permutive.rsHeaderName::as_str() is always lowercase, so it was unreachable.
  • Adding CONTENT_TYPE to didomi's forwarded headers — POST bodies can now be interpreted by upstream.

CI Status

  • browser integration tests: PASS
  • integration tests: PASS
  • prepare integration artifacts: PASS

Comment thread crates/trusted-server-core/src/integrations/prebid.rs Outdated
Comment thread crates/trusted-server-core/src/integrations/permutive.rs Outdated
Comment thread crates/trusted-server-core/src/integrations/lockr.rs Outdated
Comment thread crates/trusted-server-core/src/integrations/datadome.rs Outdated
Comment thread crates/trusted-server-core/src/integrations/aps.rs Outdated
Comment thread crates/trusted-server-core/src/integrations/mod.rs
Comment thread crates/trusted-server-core/src/auction/provider.rs
Comment thread crates/trusted-server-core/src/integrations/aps.rs
Comment thread crates/trusted-server-core/src/integrations/testlight.rs
Comment thread crates/trusted-server-core/src/integrations/mod.rs
prk-Jr added 2 commits April 16, 2026 13:07
Conflicts resolved by keeping PR13's http-native integration layer
throughout — all compat shim insertions from PR12 are superseded by
this branch's completed integration type migration.
- 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
@prk-Jr prk-Jr requested a review from aram356 April 16, 2026 12:53
Copy link
Copy Markdown
Collaborator

@aram356 aram356 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Re-review of the EdgeZero PR13 integration/provider HTTP type migration after the latest "Address review findings" commit. The async parse_response migration and the new collect_body / collect_body_bounded helpers landed cleanly and resolved the prior into_bytes()-on-Stream panic class across all providers. Two blockers remain plus one CI/process question that affects whether the existing test plan can be trusted.

Blocking

🔧 wrench

  • cargo fmt --all -- --check fails locally in 7+ files. The PR description's [x] cargo fmt checkbox is incorrect — see the CI question below for why this wasn't caught. Affected files:

    • crates/trusted-server-core/src/auction/endpoints.rs:39-47
    • crates/trusted-server-core/src/auction/orchestrator.rs:12-14, 402-405
    • crates/trusted-server-core/src/integrations/google_tag_manager.rs:16-25, 39-45
    • crates/trusted-server-core/src/integrations/lockr.rs:282-285
    • crates/trusted-server-core/src/integrations/permutive.rs:208-219, 278-281
    • crates/trusted-server-core/src/integrations/prebid.rs:5-13, 1154-1162
    • crates/trusted-server-core/src/integrations/testlight.rs:11-16

    Fix: run cargo fmt --all.

  • Testlight POST body is unbounded (inline at integrations/testlight.rs:181).

❓ question

  • CI gates aren't enforced for this PR. .github/workflows/format.yml and .github/workflows/test.yml only fire on pull_request: branches: [main]. PR 626's base is feature/edgezero-pr12-handler-layer-types, so on commit 94ec9477 only "Integration Tests" ran — fmt, clippy, and cargo test --workspace did not. Combined with the fmt failure above, every stacked EdgeZero PR ships with broken formatting and an unverified test plan. Was this intentional, or should the workflow triggers be widened (or the gates duplicated under the integration-tests umbrella)? Until that's resolved, the PR description's CI checkboxes can't be relied on as evidence.

    Verified locally on this commit: cargo test --workspace passes (821 tests), cargo clippy --workspace --all-targets --all-features -- -D warnings passes, cargo build --release --target wasm32-wasip1 passes, cargo fmt --all -- --check fails.

Non-blocking

🤔 thinking

  • Unbounded response-body reads via collect_body (inline at integrations/mod.rs:93 for stream-read error fidelity; cross-cutting concern listed here). The bounded variant exists for inbound bodies, but every upstream response site still drains an unbounded body to a Vec<u8>:

    • integrations/prebid.rs:1157 — Prebid Server response (highest risk: third-party hop, large payloads)
    • integrations/aps.rs:555 — APS response
    • integrations/adserver_mock.rs:375 — Mediator response
    • integrations/lockr.rs:150 — Lockr SDK fetch
    • integrations/permutive.rs:152 — Permutive SDK fetch
    • integrations/datadome.rs:302 — DataDome SDK fetch
    • integrations/google_tag_manager.rs:491 — GTM script

    Provider responses are the highest risk — a misbehaving Prebid Server / mediator can ship a multi-GB body and OOM the worker. Recommend routing all of these through collect_body_bounded with a per-callsite cap, or removing the unbounded variant in favor of a single bounded helper.

♻️ refactor

  • Auction providers still construct BackendConfig inline while integration proxies route through ensure_integration_backend. Reraising the prior-review concern with current locations:

    • integrations/aps.rs:517-528
    • integrations/adserver_mock.rs:338-348
    • integrations/prebid.rs:1131-1135

    The two paths differ on timeout (providers need a configurable per-request timeout; the proxy helper hardcodes 15s). Consider widening ensure_integration_backend to accept a timeout and reusing it from the providers, or at least documenting the divergence.

CI Status

  • fmt: FAIL locally (CI did not run for this commit — see ❓ question above)
  • clippy: PASS locally (CI did not run)
  • rust tests: PASS locally — 821 tests (CI did not run)
  • wasm32-wasip1 build: PASS locally
  • Integration Tests workflow: PASS (only workflow that ran on 94ec9477)

Comment thread crates/trusted-server-core/src/integrations/testlight.rs Outdated
Comment thread crates/trusted-server-core/src/integrations/mod.rs
Comment thread crates/trusted-server-core/src/auction/types.rs Outdated
Comment thread crates/trusted-server-core/src/auction/endpoints.rs Outdated
Comment thread crates/trusted-server-core/src/auction/README.md
prk-Jr added 2 commits April 21, 2026 09:10
…EADME

- 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
@prk-Jr prk-Jr requested a review from aram356 April 21, 2026 03:45
…/edgezero-pr13-integration-provider-type-migration
Copy link
Copy Markdown
Collaborator

@aram356 aram356 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Re-review of PR13 after the latest "Address PR review" commit (4b27087e) and the PR12 merge (a2036eb5). The async parse_response migration, the dedicated AUCTION_MAX_BODY_BYTES, the README updates, and the testlight body cap all landed cleanly. One blocker remains plus several non-blocking re-raises from prior reviews that haven't been addressed.

Local verification on this commit:

  • cargo test --workspacePASS (952 tests, 0 failures)
  • cargo clippy --workspace --all-targets --all-features -- -D warningsPASS
  • cargo build --release --target wasm32-wasip1 -p trusted-server-adapter-fastlyPASS
  • cargo fmt --all -- --checkFAIL (4 PR-introduced sites + 11 inherited from PR12 base)
  • GitHub CI (integration tests workflow) — PASS

GitHub's format.yml / test.yml still only fire on PRs targeting main, so the fmt/clippy/test gates don't run for this PR's stack. The PR description's [x] cargo fmt / [x] cargo clippy / [x] cargo test checkboxes therefore can't be relied on as evidence — see the fmt blocker below.

Blocking

🔧 wrench

  • fmt failures still present in PR-modified files. cargo fmt --all -- --check reports diffs at:

    • crates/trusted-server-adapter-fastly/src/main.rs:209 and :236
    • crates/trusted-server-core/src/integrations/prebid.rs:1057 (the line at ~1066 is too long inside the if let block)
    • crates/trusted-server-core/src/integrations/registry.rs:673

    These four sites did not exist on the PR12 base — PR13 introduced them. (11 additional fmt diffs are inherited from the PR12 base and are not this PR's responsibility, but the project-wide cargo fmt --all -- --check does fail.) Fix: cargo fmt --all.

Non-blocking

🤔 thinking

  • Provider/integration response bodies still unbounded (re-raise; inline at integrations/mod.rs:84). 7 callsites still drain upstream responses with collect_body instead of a bounded helper. Provider responses are highest risk (prebid.rs:1160, aps.rs:555, adserver_mock.rs:375).
  • Stream-read errors classified as Integration (re-raise; inline at integrations/mod.rs:93 and the bounded variant at :137). Conflates transport failure with integration semantic error; both surface as 5xx-server with the same shape. GTM has a bespoke 502 path that the shared helper doesn't.
  • Orchestrator backend-name fallback can silently lose bids (inline at auction/orchestrator.rs:337). When pending.backend_name() is None and the platform sets a different PlatformResponse::backend_name, the lookup at line 392 fails and the bid is dropped with a "Received response from unknown backend" warning. Consider a log::warn! when the fallback fires.
  • collect_body_bounded checks size after reading the chunk (inline at integrations/mod.rs:143). The effective bound is "≤ max + one_chunk", not strict ≤ max. Acceptable in practice (chunk sizes are small) but worth a comment on the helper.

♻️ refactor

  • Auction providers still construct BackendConfig inline (re-raise; inline at prebid.rs:1134, also aps.rs:517-528, adserver_mock.rs:338-348). Diverges from ensure_integration_backend because providers need a per-request timeout and the helper hardcodes 15s.
  • AuctionContext.client_info is redundant with services.client_info() (re-raise; inline at auction/types.rs:107).
  • run_auction takes services as a separate arg even though context.services exists (inline at auction/orchestrator.rs:64). They must stay in sync; an inconsistent caller breaks the auction silently.
  • Inconsistent services.client_info access (inline at didomi.rs:259). Two sites use the pub(crate) field (didomi.rs:259, auction/formats.rs:145); the rest use the method services.client_info(). Standardize on the method.

📝 note

  • Lockr/Permutive/Didomi handlers contain unreachable PUT/PATCH branches (inline at lockr.rs:202). Routes register only GET/POST.
  • StubHttpClient::send_async does not capture request headers (inline at platform/test_support.rs:297). Limits header-shape assertions for prebid/aps/adserver_mock tests.

🏕 camp site

  • testlight::Default impls can be derived (inline at testlight.rs:271).

👍 praise

  • compat:: shim eliminated from integration dispatchIntegrationProxy::handle is now http::Request<EdgeBody> end-to-end (main.rs:195, registry.rs:266). Removes a non-trivial conversion overhead per request and a class of header-mapping bugs.
  • Async parse_response migration with PlatformResponse resolves the prior into_bytes()-on-Body::Stream panic class for every provider (auction/provider.rs:38). The trait docs explicitly note why it's now async — exactly the right context for future implementors.
  • GTM 413 vs 502 distinction (google_tag_manager.rs:511-535). Clean separation between client-error and transport-error; this is the pattern the shared collect_body* helpers should follow.
  • EC-ID header-injection guard (registry.rs:673) plus regression test at registry.rs:1406. Defensive HeaderValue::from_str blocks \r/\n/\0 injection on a user-influenced ID before it lands in either the request or the response.

CI Status

  • fmt: FAIL locally (CI did not run for this commit because workflow triggers don't match the PR's base branch — see header above)
  • clippy: PASS locally (CI did not run)
  • rust tests: PASS locally (CI did not run; 952 tests)
  • wasm32-wasip1 build: PASS locally
  • Integration Tests workflow: PASS

Copy link
Copy Markdown
Collaborator

@aram356 aram356 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline annotations for the prior REQUEST_CHANGES review

Adding the per-line comments now that I've filtered them against the PR diff (the previous submission was body-only because GitHub rejected the batch when 3 of 17 comment anchors fell outside the changed hunks). These reference the same findings as #626 (review) — no new findings, just inline anchoring.

The three findings that couldn't be anchored to a diff line are tagged below for reference:

  • ♻️ refactor — run_auction takes services separately even though context.services exists. Anchored as a top-level note rather than inline because auction/orchestrator.rs:64 (the run_auction signature) is not in the diff, but the dual-pass pattern is visible at the response-handling sites (e.g. orchestrator.rs:337).
  • 📝 note — StubHttpClient::send_async does not capture request headers. crates/trusted-server-core/src/platform/test_support.rs is not in this PR's diff scope at all; the asymmetry between send (lines 266–280) and send_async (lines 297–320) limits header-shape assertions for prebid/aps/adserver_mock tests. Worth a follow-up issue.
  • 🏕 camp site — testlight::Default impls can be derived. testlight.rs:271-285 is outside the changed hunks; deriving #[derive(Default)] would replace both manual impls.

if request_signing_config.enabled {
let signer = RequestSigner::from_services(context.services)?;
let params =
SigningParams::new(request.id.clone(), request_info.host.clone(), request_info.scheme.clone());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 wrenchcargo fmt --all -- --check fails here (line is too long inside the if let Some(request_signing_config) = ... block). The PR description marks [x] cargo fmt, but a fresh local run on this commit reports diffs at:

  • crates/trusted-server-adapter-fastly/src/main.rs:209
  • crates/trusted-server-adapter-fastly/src/main.rs:236
  • crates/trusted-server-core/src/integrations/prebid.rs:1057 (this site)
  • crates/trusted-server-core/src/integrations/registry.rs:673

(11 additional fmt diffs are inherited from the PR12 base — not this PR's responsibility — but PR13 introduced the 4 above.)

Fix: cargo fmt --all.

req.set_header(HEADER_X_TS_EC, ec_id.as_str());
match HeaderValue::from_str(ec_id) {
Ok(header_value) => {
req.headers_mut().insert(HEADER_X_TS_EC.clone(), header_value);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 wrenchcargo fmt --all -- --check fails here too (req.headers_mut().insert(HEADER_X_TS_EC.clone(), header_value); needs to wrap). Same root cause as the prebid.rs:1057 site. cargo fmt --all will fix it.

/// # Errors
///
/// Returns an error when a streaming body chunk cannot be read.
pub(crate) async fn collect_body(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 thinkingcollect_body is still unbounded for upstream responses (re-raise from prior review). Inbound bodies are now correctly capped via collect_body_bounded, but every provider/integration response read still drains an unbounded body to Vec<u8>:

  • integrations/prebid.rs:1160 (Prebid Server response — highest risk: third-party hop)
  • integrations/aps.rs:555 (APS response)
  • integrations/adserver_mock.rs:375 (Mediator response)
  • integrations/lockr.rs:150 (Lockr SDK fetch)
  • integrations/permutive.rs:152 (Permutive SDK fetch)
  • integrations/datadome.rs:302 (DataDome SDK fetch)
  • integrations/google_tag_manager.rs:549 (GTM script)

A misbehaving Prebid Server / mediator can ship a multi-GB body and OOM the worker. Recommend routing all of these through collect_body_bounded with a per-callsite cap (e.g., 1–2 MiB for RTB responses, 16 MiB for SDK fetches), or removing the unbounded variant in favor of a single bounded helper.

EdgeBody::Stream(mut stream) => {
let mut body_bytes = Vec::new();
while let Some(chunk_result) = stream.next().await {
let chunk = chunk_result.map_err(|error| {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 thinking — Stream-read errors are still wrapped as TrustedServerError::Integration for both collect_body and collect_body_bounded (this site at lines 93–98 and the bounded variant at 137–142). This conflates "transport failed mid-body" with "integration semantic error" — both surface as 5xx-server-error with the same shape.

GTM has a bespoke PayloadSizeError::StreamRead path that returns 502 explicitly (google_tag_manager.rs:528). The shared helper doesn't get that distinction.

Fix: introduce a dedicated variant (e.g. TrustedServerError::UpstreamBodyRead) that maps to 502, or document the 5xx mapping explicitly so callers know what they're returning to the client. Re-raise from prior review.

message: format!("Failed to read request body: {error}"),
})
})?;
if body_bytes.len() + chunk.len() > max_bytes {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 thinking — The size check happens after chunk is materialized in memory, not before. If a single chunk's len() is greater than max_bytes, that chunk has already been allocated and read before being rejected.

In practice Fastly H2/H3 framing chunks are small (~16 KiB), and the EdgeBody::Once path at line 124 already short-circuits on a single-shot body. So this is a small concern, but worth a comment: the bound here is "≤ max + one_chunk" rather than a hard ≤ max.


let mut target_req = Request::new(method.clone(), &target_url);
self.copy_request_headers(&req, &mut target_req);
let request_body = if matches!(method, Method::POST | Method::PUT | Method::PATCH) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 note — Routes register only GET /sdk, GET /api/*, POST /api/* (line 374), but the body guard here matches POST | PUT | PATCH. PUT/PATCH cannot reach the handler, so those branches are dead defensive code.

Same pattern in permutive.rs:210 and didomi.rs:242. Either tighten the match to POST only, or also register PUT/PATCH routes if the upstream API actually needs them.

})
.map(compat::from_fastly_response)
}
(m, path) if integration_registry.has_route(&m, path) => integration_registry
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 praise — Clean elimination of the compat::to_fastly_request / compat::from_fastly_response round-trip for integration dispatch. The proxy registry now operates on http::Request<EdgeBody> end-to-end, which removes a non-trivial amount of conversion overhead per request and eliminates an entire class of header-mapping bugs.

) -> Result<PlatformPendingRequest, Report<TrustedServerError>>;

/// Parse the response from the provider into an `AuctionResponse`.
///
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 praise — Async parse_response migration with PlatformResponse resolves the prior unit-asserted into_bytes()-on-Body::Stream panic class for every provider. The trait doc comment explicitly explains why it's now async ("so implementations can safely drain streaming response bodies without panicking on the Body::Stream variant") — exactly the right note for future implementors.


log::debug!("Proxying to upstream: {}", target_url);

// Handle payload size errors explicitly to return 413 instead of 502
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 praise — Correct 413 vs 502 distinction. PayloadSizeError::TooLarge → 413 (client error); PayloadSizeError::StreamRead → 502 (transport error). This addresses the prior reviewer's misclassification finding cleanly, and is the model the shared collect_body* helpers should follow.

// Header injection: Fastly's HeaderValue API rejects values containing \r, \n, or \0,
// Header injection: HeaderValue::from_str rejects values containing \r, \n, or \0,
// so a crafted EC ID cannot inject additional request headers.
if let Ok(ref ec_id) = ec_id_result {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 praise — Defensive HeaderValue::from_str guard against \r/\n/\0 injection on the EC ID before it lands in the request and the response, with a regression test at line 1406 (handle_proxy_replaces_invalid_ec_request_header_with_matching_response_cookie). Good security posture.

Copy link
Copy Markdown
Collaborator

@aram356 aram356 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline annotations for the prior REQUEST_CHANGES review

Adding the per-line comments now that I've filtered them against the PR diff (the previous submission was body-only because GitHub rejected the batch when 3 of 17 comment anchors fell outside the changed hunks). These reference the same findings as #626 (review) — no new findings, just inline anchoring.

The three findings that couldn't be anchored to a diff line are tagged below for reference:

  • ♻️ refactor — run_auction takes services separately even though context.services exists. Anchored as a top-level note rather than inline because auction/orchestrator.rs:64 (the run_auction signature) is not in the diff, but the dual-pass pattern is visible at the response-handling sites (e.g. orchestrator.rs:337).
  • 📝 note — StubHttpClient::send_async does not capture request headers. crates/trusted-server-core/src/platform/test_support.rs is not in this PR's diff scope at all; the asymmetry between send (lines 266–280) and send_async (lines 297–320) limits header-shape assertions for prebid/aps/adserver_mock tests. Worth a follow-up issue.
  • 🏕 camp site — testlight::Default impls can be derived. testlight.rs:271-285 is outside the changed hunks; deriving #[derive(Default)] would replace both manual impls.

if request_signing_config.enabled {
let signer = RequestSigner::from_services(context.services)?;
let params =
SigningParams::new(request.id.clone(), request_info.host.clone(), request_info.scheme.clone());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 wrenchcargo fmt --all -- --check fails here (line is too long inside the if let Some(request_signing_config) = ... block). The PR description marks [x] cargo fmt, but a fresh local run on this commit reports diffs at:

  • crates/trusted-server-adapter-fastly/src/main.rs:209
  • crates/trusted-server-adapter-fastly/src/main.rs:236
  • crates/trusted-server-core/src/integrations/prebid.rs:1057 (this site)
  • crates/trusted-server-core/src/integrations/registry.rs:673

(11 additional fmt diffs are inherited from the PR12 base — not this PR's responsibility — but PR13 introduced the 4 above.)

Fix: cargo fmt --all.

req.set_header(HEADER_X_TS_EC, ec_id.as_str());
match HeaderValue::from_str(ec_id) {
Ok(header_value) => {
req.headers_mut().insert(HEADER_X_TS_EC.clone(), header_value);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 wrenchcargo fmt --all -- --check fails here too (req.headers_mut().insert(HEADER_X_TS_EC.clone(), header_value); needs to wrap). Same root cause as the prebid.rs:1057 site. cargo fmt --all will fix it.

/// # Errors
///
/// Returns an error when a streaming body chunk cannot be read.
pub(crate) async fn collect_body(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 thinkingcollect_body is still unbounded for upstream responses (re-raise from prior review). Inbound bodies are now correctly capped via collect_body_bounded, but every provider/integration response read still drains an unbounded body to Vec<u8>:

  • integrations/prebid.rs:1160 (Prebid Server response — highest risk: third-party hop)
  • integrations/aps.rs:555 (APS response)
  • integrations/adserver_mock.rs:375 (Mediator response)
  • integrations/lockr.rs:150 (Lockr SDK fetch)
  • integrations/permutive.rs:152 (Permutive SDK fetch)
  • integrations/datadome.rs:302 (DataDome SDK fetch)
  • integrations/google_tag_manager.rs:549 (GTM script)

A misbehaving Prebid Server / mediator can ship a multi-GB body and OOM the worker. Recommend routing all of these through collect_body_bounded with a per-callsite cap (e.g., 1–2 MiB for RTB responses, 16 MiB for SDK fetches), or removing the unbounded variant in favor of a single bounded helper.

EdgeBody::Stream(mut stream) => {
let mut body_bytes = Vec::new();
while let Some(chunk_result) = stream.next().await {
let chunk = chunk_result.map_err(|error| {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 thinking — Stream-read errors are still wrapped as TrustedServerError::Integration for both collect_body and collect_body_bounded (this site at lines 93–98 and the bounded variant at 137–142). This conflates "transport failed mid-body" with "integration semantic error" — both surface as 5xx-server-error with the same shape.

GTM has a bespoke PayloadSizeError::StreamRead path that returns 502 explicitly (google_tag_manager.rs:528). The shared helper doesn't get that distinction.

Fix: introduce a dedicated variant (e.g. TrustedServerError::UpstreamBodyRead) that maps to 502, or document the 5xx mapping explicitly so callers know what they're returning to the client. Re-raise from prior review.

message: format!("Failed to read request body: {error}"),
})
})?;
if body_bytes.len() + chunk.len() > max_bytes {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 thinking — The size check happens after chunk is materialized in memory, not before. If a single chunk's len() is greater than max_bytes, that chunk has already been allocated and read before being rejected.

In practice Fastly H2/H3 framing chunks are small (~16 KiB), and the EdgeBody::Once path at line 124 already short-circuits on a single-shot body. So this is a small concern, but worth a comment: the bound here is "≤ max + one_chunk" rather than a hard ≤ max.

*pbs_req.body_mut() = EdgeBody::from(pbs_body);

// Send request asynchronously with auction-scoped timeout
let backend_name = BackendConfig::from_url_with_first_byte_timeout(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ refactor — Re-raise from prior review: providers still construct BackendConfig inline while integration proxies route through ensure_integration_backend. Other instances:

  • integrations/aps.rs:517-528
  • integrations/adserver_mock.rs:338-348

The two paths differ on timeout — providers need context.timeout_ms per request, the proxy helper hardcodes 15s.

Fix: widen ensure_integration_backend to accept a Duration, or split into ensure_proxy_backend(15s) / ensure_provider_backend(timeout). Alternatively, document the divergence explicitly.

pub settings: &'a Settings,
pub request: &'a Request,
pub request: &'a Request<EdgeBody>,
pub client_info: &'a ClientInfo,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ refactor — Re-raise from prior review: client_info is redundant with services.client_info().

AuctionContext now carries both client_info: &'a ClientInfo and services: &'a RuntimeServices, and RuntimeServices::client_info() already exposes the same value. auction/endpoints.rs:105-108 populates both. Two paths to the same data invites drift.

Fix: drop the client_info field; callers use context.services.client_info().

match provider.request_bids(request, &provider_context) {
match provider.request_bids(request, &provider_context).await {
Ok(pending) => {
let request_backend_name = pending
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 thinkingpending.backend_name().unwrap_or_else(|| backend_name.clone()) silently falls back to the predicted name. If the runtime returns None for pending.backend_name() and PlatformResponse::backend_name is set to something else by the platform impl, the response handler at line 392 will fail to identify the provider and log "Received response from unknown backend" — meaning that provider's bids are silently dropped.

Worth a log::debug! (or warn!) when the fallback fires, so production has a signal if PlatformPendingRequest::backend_name() ever stops returning the expected value.

.change_context(Self::error("Failed to build Didomi proxy request"))?;
self.copy_headers(
&backend,
services.client_info.client_ip,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ refactor — Inconsistent access: this reaches the pub(crate) field directly (services.client_info.client_ip) while auction/endpoints.rs:72, auction/endpoints.rs:105, and prebid.rs:1378 use the public method (services.client_info()). One other field-access site is at auction/formats.rs:145.

Fix: standardize on the method (services.client_info().client_ip).


let mut target_req = Request::new(method.clone(), &target_url);
self.copy_request_headers(&req, &mut target_req);
let request_body = if matches!(method, Method::POST | Method::PUT | Method::PATCH) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 note — Routes register only GET /sdk, GET /api/*, POST /api/* (line 374), but the body guard here matches POST | PUT | PATCH. PUT/PATCH cannot reach the handler, so those branches are dead defensive code.

Same pattern in permutive.rs:210 and didomi.rs:242. Either tighten the match to POST only, or also register PUT/PATCH routes if the upstream API actually needs them.

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.

Integration + provider layer type migration

3 participants