Skip to content

Distinguish empty PBS demand from stored-request intent #1086

Description

@ChristianPavilonis

Description

TSJS injects a synthetic trustedServer bidder into every ad unit so providers configured with routing = "all_eligible", such as APS, can participate even when the publisher did not configure inline server-side bidders for that slot.

When such a slot has no plan-owned bidder parameters, TSJS sends this envelope:

{
  "bidderParams": {}
}

The auction router currently treats missing, null, and empty bidderParams as intentional Prebid Server stored-request demand. It routes the slot to every configured prebid-server provider and builds:

{
  "ext": {
    "prebid": {
      "storedrequest": {
        "id": "<dynamic-slot-code>"
      }
    }
  }
}

For a dynamic slot with no matching stored impression, PBS returns HTTP 400. That rejects the provider's complete OpenRTB request, including otherwise valid impressions with inline bidder parameters. Multiple PBS provider instances repeat the same failing stored-request lookup. Other all_eligible providers can still succeed independently.

An empty bidder map is ambiguous. It can mean either "invoke all-eligible providers with no PBS demand" or "load a PBS stored impression." The router should not infer both cases from the same shape.

This was observed while validating #1016, but it is unrelated to per-provider OpenRTB test: 1 behavior.

Suggested fix

Add explicit stored-request intent to the trustedServer envelope, for example:

{
  "bidderParams": {},
  "storedRequest": false
}

TSJS-generated envelopes used only to invoke all-eligible providers should set storedRequest: false. Routing should then skip every prebid-server provider that has neither routed inline parameters nor stored-request intent, while still admitting eligible non-PBS all_eligible providers such as APS. A PBS provider configured as all_eligible still has no useful request to make when both inline and stored demand are absent.

Intentional PBS stored demand should use storedRequest: true. Omitted values can preserve the legacy stored-request behavior during migration if compatibility requires it. Direct server-generated opportunities should continue to use trusted server-owned routing rather than allowing the browser to select a provider.

Wire compatibility and internal model

Keep storedRequest optional on the wire so the server can distinguish all three states:

Wire value Meaning
false No stored demand. Skip PBS providers without routed inline parameters.
true Explicit stored-request intent.
omitted Legacy inference during migration.

Represent this internally as an enum such as Disabled, Explicit, and Legacy, rather than collapsing omitted and false into a Rust boolean. Deploy server support before TSJS starts emitting storedRequest: false; an older server will otherwise continue treating the empty envelope as stored demand.

Stored request identity

The minimal boolean fix can continue deriving the stored impression ID from the slot code. A later design should consider an explicit shape such as storedRequest: { "id": "homepage-header" } or storedRequestId: "homepage-header", because dynamic display codes are poor stored-request identifiers. Multiple PBS provider instances also need server-owned provider selection for intentional stored demand instead of automatic fanout. Those changes do not need to block the minimal ambiguity fix.

Regression coverage should include:

  • an APS all-eligible slot with no PBS bidder parameters, which reaches APS and skips every PBS provider without demand;
  • explicit stored-request intent, which retains the existing PBS path;
  • inline bidders split across multiple PBS provider instances;
  • one no-PBS slot alongside valid inline PBS impressions, proving it cannot cause a provider-wide HTTP 400;
  • storedRequest: false with empty bidder parameter objects;
  • storedRequest: false with only unconfigured bidders after route filtering;
  • a malformed non-boolean storedRequest, which must fail validation atomically and never trigger legacy stored demand;
  • an omitted value from a legacy direct /auction caller, which preserves the existing stored-request path;
  • TSJS serialization of storedRequest: false for an empty all-eligible envelope; and
  • a prebid-server provider configured as all_eligible, which is still skipped when the slot has neither inline nor stored demand.

Metadata

Metadata

Labels

No labels
No labels

Type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions