Skip to content

July release candidate (DO NOT MERGE) - #919

Draft
ChristianPavilonis wants to merge 275 commits into
mainfrom
rc/july
Draft

July release candidate (DO NOT MERGE)#919
ChristianPavilonis wants to merge 275 commits into
mainfrom
rc/july

Conversation

@ChristianPavilonis

@ChristianPavilonis ChristianPavilonis commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

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 main

Pending Review / Merge

prk-Jr and others added 30 commits July 14, 2026 23:26
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.
Allow operators to retain sanitizer-accepted external URLs in POST /auction adm while preserving mandatory server-side sanitization and the existing default behavior.
# 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.
ChristianPavilonis and others added 7 commits July 29, 2026 12:20
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.
Comment thread crates/trusted-server-core/src/integrations/gpt_bootstrap.js Fixed
aram356 and others added 4 commits July 29, 2026 14:59
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 and others added 9 commits July 30, 2026 08:20
# 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.
window.open(resolved, target, 'noopener,noreferrer');
} else {
location.href = url;
location.href = resolved;
if (canonFromFirstPartyClick(finalUrl)) {
el.setAttribute('data-tsclick', finalUrl);
}
el.setAttribute('href', finalUrl);
aram356 added 7 commits July 31, 2026 02:03
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.
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.

4 participants