July release candidate (DO NOT MERGE) - #919
Draft
ChristianPavilonis wants to merge 275 commits into
Draft
Conversation
A configured bidder with no inline params and no matching override
expanded to `"bidder": {}`, which PBS rejects. After applying overrides,
drop fabricated empty bidders, preserve an explicitly supplied empty
object so genuine misconfiguration stays visible, and fall back to the
stored-request path when no eligible bidders remain.
…debug' into merge/pr-890-893-899-910-912
…to merge/pr-890-893-899-910-912
…' into merge/pr-890-893-899-910-912
…-params' into merge/pr-890-893-899-910-912
…o merge/pr-890-893-899-910-912
Allow operators to retain sanitizer-accepted external URLs in POST /auction adm while preserving mandatory server-side sanitization and the existing default behavior.
…ing' into merge/pr-890-893-899-910-912
# Conflicts: # CHANGELOG.md # crates/trusted-server-core/src/auction/formats.rs # crates/trusted-server-core/src/integrations/aps.rs # crates/trusted-server-core/src/publisher.rs # docs/guide/auction-orchestration.md
…code cached bids The inline-adm path fed raw bid.creative into window.tsjs.bids, bypassing the creative-processing boundary the /auction path applies (sanitize_creative_html then rewrite_creative_html, which also enforces the 1 MiB creative cap). Run the same boundary in build_bid_map before inserting adm; omit adm when the creative is rejected so the bridge falls back to the PBS Cache coordinates. Thread &Settings through build_bid_map and write_bids_to_state for rewrite_creative_html. The pbRender bridge's PBS Cache fallback forwarded the raw GET body as the ad, but PBS Cache returns a JSON bid object and the Prebid Universal Creative renders bidObject.adm. Add extractCachedAdm to parse the cached bid and extract its adm, keep raw-markup compatibility, and decline to render when no adm is present. Add hostile and oversized creative coverage in Rust, and realistic returnCreative=false, malformed, and raw-markup cache-response coverage in JS. Update the inject_adm_for_testing, AuctionBidData, GPT bridge, and design-spec docs to distinguish production inline adm from the testing-only debug_bid path.
Resolve conflicts in favor of the streaming pipeline: - app.rs: keep the lazy publisher_response_into_streaming_response import; Fastly streams, so buffer_publisher_response_async is unused here. - publisher.rs: keep module-level flate2/futures imports and the body.is_stream() streaming branch in stream_html_with_auction_hold. Main moved core to edition 2024 via workspace inheritance, enabling let-chains and tripping clippy::collapsible_if; collapse the nested if in passthrough_finish_segments to satisfy it.
BodyStreamEncoder::encode_chunk wrote each processed chunk into the gzip/deflate/brotli codec and drained the inner buffer, but never flushed the codec. Compressors buffer internally: write_all alone left gzip emitting at most its 10-byte header and deflate/brotli emitting nothing until finish(). Fastly commits the response headers early, so the browser saw an open stream with no decodable HTML until the entire origin transfer completed — the FCP regression tracked in #849. Sync-flush the flate2 encoders (Flush::Sync — byte-aligned, no trailer) and emit a brotli flush marker (BROTLI_OPERATION_FLUSH) after each write. finish() still writes the terminating trailer, so the full stream stays valid. Add a regression test that decodes the first chunk with a flushed-but-unfinished decoder for every codec.
The close-body hold step processed a chunk into ready segments and the held closing tail together, but awaited collect_stream_auction() before returning any of them. When </body> landed in the first source chunk (common for small pages), the client received no HTML until the auction finished; for larger pages the whole prefix from the close-containing chunk was delayed — contradicting the contract that only the </body> tail is held while the auction rides alongside transfer. Split hold_step_decoded_chunk so it returns the ready prefix plus a close_found flag without collecting, and move collection plus held-tail processing into a new hold_collect_close_tail. Both the lazy Fastly stream and the write-sink driver now emit the ready prefix first, then await collection, then emit the tail. Add a regression test asserting the prefix is ready with close_found set while the auction is still uncollected (ad_bids_state stays None until the tail step).
Both publisher finalizers dropped the body for bodiless responses but kept the origin Content-Length verbatim, including a possible nonzero value. RFC 9110 §8.6 forbids Content-Length on 204, and a nonzero length on a now-empty 205 (§15.4.6) is invalid; inconsistent framing risks interoperability failures and response-splitting across intermediaries. Add make_response_bodiless: it empties the body and removes Content-Length for 204, normalizes it to 0 for 205, and preserves it for HEAD and 304 (which legitimately advertise the GET representation length). Call it from the buffered-unmodified arm of both buffer_publisher_response_async and publisher_response_into_streaming_response. Split the combined bodiless regression test into per-status expectations and add the matching coverage for the buffered finalizer.
A conditional navigation can dispatch a server-side auction and receive a processable HTML 304 that classification routes to Stream. Both finalizers' bodiless branches logged a warning and returned, dropping params.dispatched_auction without emit_abandoned_auction — so the SSP work and quota consumption had no terminal auction event, leaving a gap in auction observability on a legitimate conditional-request path. Take the dispatched auction in the bodiless branch of both buffer_publisher_response_async and publisher_response_into_streaming_response and await emit_abandoned_auction(..., "bodiless_response") with the retained observation, matching the processor-init-error path. Add a recording-telemetry-sink test (via a new test_support helper) asserting both finalizers emit the bodiless_response abandonment for a 304 with a dispatched auction.
The merge moved core to edition 2024, whose rustfmt sorts use-imports case-sensitively (lowercase items last) and rewraps long lines. Reformat the conflict-resolved imports and the new test assertions to match; no behavior change.
Address the auction transport-timeout review findings: - Derive dynamic backend names from a bounded readable prefix plus a SHA-256 digest of the complete backend spec, so distinct specs never alias to one name. Name equality now implies spec equality, making NameInUse reuse provably safe, and the name is bounded to Fastly's 255-char limit regardless of host or discriminator length. - Bound budget-derived transport-timeout cardinality with a globally finite ladder: keep the 250ms quantum through 2000ms, then snap larger budgets to a small fixed set of coarse rungs so a large configured ceiling can no longer mint hundreds of dynamic backends. - Reject duplicate configured providers at startup, and check the predicted backend name before request_bids fires the outbound send, retaining the post-launch check as defense against a provider that resolves to an unexpected name.
…sport-timeouts # Conflicts: # crates/trusted-server-adapter-fastly/src/backend.rs # crates/trusted-server-core/src/auction/orchestrator.rs
The backend name now carries a SHA-256 digest suffix, so the three platform predict_name tests that pinned the exact/ending name string no longer hold. Assert the readable body via starts_with/contains instead. These were missed earlier because the local run filtered the platform suite too narrowly; CI aborted on the first panic.
…sanitization-optional
Publishers may call googletag.display with a slot or element before the initial request gate is released. Treating that argument as a string can abort the request, while first-prefix slot resolution can target a hidden responsive sibling. Preserve the GPT display contract and resolve stable prefixes only when one active element can be identified.
APS bids are bid-by-reference: interpretResponse sets the renderer descriptor on the Prebid bid as the custom trustedServerRenderer field, and a bidResponse listener registers it in window.tsjs.apsPrebidRenderers keyed by Prebid's generated adId so the Universal Creative can later request it. Prebid normalizes each bid into its own object during addBidResponse and drops unknown top-level fields, so the custom field can be gone before the bidResponse listener runs (observed in production: absent as early as bidAccepted). The listener then saw renderer === undefined and returned without registering, leaving the registry empty; the Universal Creative's request found nothing and Prebid's default renderer threw "Missing ad markup or URL" (reason noAd) for every APS bid. Carry the descriptor in bid.meta as well. meta is a first-class field Prebid preserves through normalization (bidderFactory assigns bid.meta onto the normalized bid), and it is per bid, so multiple APS bids on the same imp each keep their own descriptor. The bidResponse listener falls back to the meta copy when the top-level field is absent and scrubs both copies unconditionally after the registration attempt, keeping the executable capability only in the bounded one-time registry. Adds unit tests for registration via meta with the top-level field stripped, distinct renderer registration for multiple APS bids on one imp, and that a stripped bid carrying no descriptor registers nothing.
# Conflicts: # crates/trusted-server-js/lib/src/integrations/prebid/index.ts # crates/trusted-server-js/lib/test/integrations/prebid/index.test.ts
# Conflicts: # crates/trusted-server-js/lib/src/integrations/prebid/index.ts # crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts # crates/trusted-server-js/lib/test/integrations/prebid/index.test.ts
ChristianPavilonis
force-pushed
the
rc/july
branch
from
July 30, 2026 13:05
0325868 to
5d36b2d
Compare
# Conflicts: # CHANGELOG.md # crates/trusted-server-cli/tests/config_env_overlay.rs # crates/trusted-server-core/src/auction/README.md # crates/trusted-server-core/src/auction/formats.rs # crates/trusted-server-core/src/auction/orchestrator.rs # crates/trusted-server-core/src/auction_config_types.rs # crates/trusted-server-core/src/config_payload.rs # crates/trusted-server-core/src/creative.rs # crates/trusted-server-core/src/publisher.rs # crates/trusted-server-core/src/settings.rs # docs/guide/auction-orchestration.md # docs/guide/configuration.md # docs/guide/creative-processing.md # trusted-server.example.toml
# Conflicts: # crates/trusted-server-core/src/integrations/gpt_bootstrap.js # crates/trusted-server-js/lib/src/integrations/gpt/index.ts # crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts
Reconciles the squash-merged #945 (deferred initial adInit) with the RC's own evolution of the same files: - build_bids_script, the bundle scheduler, and the gpt_bootstrap.js fallback take main's final form: the SSR bids payload is handed to scheduleInitialAdInit, which applies it and runs adInit only on navigation generation 0; adInit rechecks its generation inside the queued googletag.cmd callback; the head bootstrap keeps the bundle-failure fallback scheduler. - The RC's pathname-plus-query route identity is kept: docblocks and the query-change tests follow the RC semantics (a query change requests fresh page bids and cancels the pending initial pass), and the RC's slot-handoff/initial-request-gate machinery is preserved alongside the new scheduler. - gpt_bootstrap.test.ts is adapted to the RC bootstrap: assertions target the pre-patch spies (the handoff patcher wraps defineSlot/display/ refresh) and expect the TS fallback slot on the publisher's actual inner div per the RC handoff design.
Both branches fixed oversized HEAD response metadata independently with mirrored parameter designs. Adopt main's implementation wholesale — the request_is_head parameter plus response_carries_body(), which also covers 1xx/204/205/304 bodiless statuses — and keep the RC's cache-bypass feature intact: send() retains its bypass_cache read alongside the new request_is_head, and the RC's apply_fastly_cache_bypass tests are restored next to main's bodiless-metadata tests.
…covery Address the second review round: - Keep rewrite_creatives defaulting to true. The measurement behind this PR justifies making sanitization opt-in; flipping rewriting too would silently disable first-party proxying for every existing deployment and invert the rollback serialization contract. Only sanitize_creatives (default false) is new behavior. - Suppress the PBS Cache fallback when a supplied creative is rejected by processing (script-only after sanitize, malformed, or over the 1 MiB cap). The GPT bridge renders the cached bid's original adm, so retaining the hb_cache_* coordinates would deliver exactly the markup processing refused. Cache coordinates now ship only for bids that supplied no creative. - Parse the GET /first-party/proxy-rebuild query from uri().query() instead of the full URI: browsers via the Axum and Spin adapters deliver origin-form URIs, which url::Url::parse rejected before the 302 could be issued. - Strip bidder <base> elements in the rewrite pass itself. Rewriting emits root-relative first-party URLs, and a bidder base href would rebase them onto a third-party origin; sanitization also removes <base> but is independently optional. - Resolve click-guard URLs against a pinned document.baseURI instead of location.href, which is about:srcdoc inside the sandboxed creative iframe and rejects root-relative signed click URLs as a base. Navigation URLs are absolutized the same way. - Never write the GET rebuild fallback into data-tsclick: it is the canonical signed click used to diff later mutation waves. href updates, the canonical source persists, and a second mutation wave still rebuilds. - Share one hasOpaqueOrigin predicate between the click guard and dynamic signing; reference issue #982 (parent postMessage broker) from the code and the creative-processing guide's new runtime-protections note. - Document upgrade sequencing (binary first, then config: old binaries reject blobs carrying sanitize_creatives), the per-mode 1 MiB cap, and the requirement to add both TOML leaves before environment overlays apply; add CLI coverage for the sanitize overlay.
- Register and scrub the APS renderer descriptor on bidAccepted, the first event after Prebid assigns adId, so bidResponse and analytics consumers of later events never observe the meta copy; the bidResponse pass remains as a fallback that no-ops once the bidAccepted pass has scrubbed. - Guard the meta read with a narrow object check so a module overwriting meta with a non-object value cannot break registration or scrubbing. - Consolidate the dual-carrier rationale onto APS_RENDERER_FIELD, stating the belt-and-braces intent of keeping the custom top-level copy; both call sites now reference it in one line.
Aligns the rc/july copy with the cleaned version on the responsive-slot display gating branch: the function early-returns when divId is absent, so the per-candidate re-check always evaluated to true.
Adopts the gating revert and the hardened GPT slot handoff from #978: - Removes the publisher initial-request gate (initialRequestGate, heldPublisherRequests, GptInitialRequestGate) that #978 reverted - Takes matchingHandoff/displayTargetElementId and the responsive-slot helpers with ambiguous-hydration protection - Keeps rc-only content intact: gpt_diagnostics types, the #948 disableInitialLoad sync (syncInitialLoadDisabled wired into the refresh-selection path), and the #945 scheduleInitialAdInit coverage - Drops the obsolete held-display test from schedule_initial_ad_init and ports the two #948 setConfig tests to the new zero-arg runGptBootstrap harness
The APS adapter spec loads the external Prebid bundle and assumed it carries the tsjs shim (and the trustedServer adapter) inside it. Once the shim is decoupled (PR #967) the external bundle is pure Prebid.js, no adapter ever registers, and the in-page auction times out. Detect the decoupled world through the window.__tsjs_prebid_bundle manifest stamp and load dist/tsjs-prebid.js after the external bundle, matching the script order the server actually serves. On a coupled bundle the stamp is absent and the spec behaves exactly as before.
The APS adapter spec loads the external Prebid bundle and assumed it carries the tsjs shim (and the trustedServer adapter) inside it. Once the shim is decoupled (PR #967) the external bundle is pure Prebid.js, no adapter ever registers, and the in-page auction times out. Detect the decoupled world through the window.__tsjs_prebid_bundle manifest stamp and load dist/tsjs-prebid.js after the external bundle, matching the script order the server actually serves. On a coupled bundle the stamp is absent and the spec behaves exactly as before.
Brings in the #988 browser-spec fix through its PR lineage and, because #988 stacks on #963's head, refreshes rc's stale #963 absorption with the July 29-30 rework: - bid.meta second descriptor carrier and bidAccepted registration replacing the requestId stash (prebid shim and Rust provider) - Hardened APS auction delivery: sanitized publisher page identity (query/fragment stripped), delivery drop telemetry with dropped_winner_count/reasons, imp disposition counters - ProviderLaunchState/ProviderRequestOutcome orchestrator refactor with parse_state threading and Immediate outcomes - as_aps() Option accessor, fail-closed render-bridge stop, responsive slot-root helpers, case-insensitive APS exclusion tests Preserved rc-only systems the #963 branch predates: #956 opt-in creative processing (process_auction_creative, sanitize_creatives), #967 decoupled prebid shim (public markWinningBidAsUsed instead of prebid.js internals), #948/#912 GPT sync, #865 platform timeout canonicalization (restored at both launch paths and both mediator paths, with the duplicate backend-name pre/post-launch guards and their test suite ported to the new provider API), and the provider-validation startup checks.
…c/july Adopts main's finalized #974 GPT diagnostics (standalone ts_console-gated module, finalize_response wiring, updated overlay/store/badges/binding) over rc's earlier absorbed copy, and #984's whole-package eslint gate (eslint . --max-warnings=0). Keeps rc-only content where the two lint passes collided: the #963/#967/#988 test rewrites in the prebid, APS, request, and ad_init suites, the GptSlotHandoff type, and the unexpected-origin-304 guard alongside main's diagnostics finalize call in publisher.rs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Consolidates the July release-candidate changes for review and integration testing before the individual changes merge to
main.Included pull requests
Already included through
mainPending Review / Merge