From 7df133cfa81fd308c684c71dfd4de9c89557d6aa Mon Sep 17 00:00:00 2001 From: "mike.hoyt" Date: Mon, 4 May 2026 15:28:09 -0600 Subject: [PATCH 1/4] Rename ext.prebid to ext.openads while allowing both --- endpoints/openrtb2/auction.go | 20 ++- endpoints/openrtb2/auction_test.go | 65 +++++++++ exchange/exchange_test.go | 34 +++-- .../exchangetest/aliases-openads-key.json | 125 +++++++++++++++++ .../passthrough_root_and_imp_openads_key.json | 130 ++++++++++++++++++ exchange/utils.go | 11 +- exchange/utils_test.go | 32 ++++- openrtb_ext/device.go | 4 + openrtb_ext/floors.go | 29 ++++ openrtb_ext/floors_test.go | 59 ++++++++ openrtb_ext/request.go | 27 ++++ openrtb_ext/request_test.go | 62 +++++++++ openrtb_ext/request_wrapper.go | 22 ++- openrtb_ext/request_wrapper_test.go | 65 +++++++++ 14 files changed, 666 insertions(+), 19 deletions(-) create mode 100644 exchange/exchangetest/aliases-openads-key.json create mode 100644 exchange/exchangetest/passthrough_root_and_imp_openads_key.json diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index 0eaf0e302..f6ac6dfd9 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -1778,7 +1778,7 @@ func (deps *endpointDeps) processStoredRequests(requestJson []byte, impInfo []Im } // Extract Passthrough from Merged Imp - passthrough, _, _, err := jsonparser.Get(resolvedImp, "ext", "prebid", "passthrough") + passthrough, _, _, err := getExtPrebidValue(resolvedImp, "passthrough") if err != nil && err != jsonparser.KeyPathNotFoundError { return nil, nil, []error{err} } @@ -1814,7 +1814,7 @@ func (deps *endpointDeps) processStoredRequests(requestJson []byte, impInfo []Im func parseImpInfo(requestJson []byte) (impData []ImpExtPrebidData, errs []error) { if impArray, dataType, _, err := jsonparser.Get(requestJson, "imp"); err == nil && dataType == jsonparser.Array { _, _ = jsonparser.ArrayEach(impArray, func(imp []byte, _ jsonparser.ValueType, _ int, _ error) { - impExtData, _, _, _ := jsonparser.Get(imp, "ext", "prebid") + impExtData, _, _, _ := getExtPrebidValue(imp) var impExtPrebid openrtb_ext.ExtImpPrebid if impExtData != nil { if err := jsonutil.Unmarshal(impExtData, &impExtPrebid); err != nil { @@ -1828,6 +1828,20 @@ func parseImpInfo(requestJson []byte) (impData []ImpExtPrebidData, errs []error) return } +// getExtPrebidValue reads from the prebid block of the given +// JSON. Top-level alias resolution: when ext.openads exists it is used +// in full; otherwise ext.prebid is used. ext.prebid is never consulted +// when ext.openads exists, even for subkeys missing from openads. +// Outbound emission is unaffected. +func getExtPrebidValue(data []byte, subKeys ...string) ([]byte, jsonparser.ValueType, int, error) { + rootKey := openrtb_ext.PrebidExtKey + if _, dt, _, err := jsonparser.Get(data, "ext", openrtb_ext.OpenAdsExtKey); err == nil && dt != jsonparser.NotExist { + rootKey = openrtb_ext.OpenAdsExtKey + } + path := append([]string{"ext", rootKey}, subKeys...) + return jsonparser.Get(data, path...) +} + type ImpExtPrebidData struct { Imp json.RawMessage ImpExtPrebid openrtb_ext.ExtImpPrebid @@ -1838,7 +1852,7 @@ type ImpExtPrebidData struct { // (e.g. malformed json, id not a string, etc). func getStoredRequestId(data []byte) (string, bool, error) { // These keys must be kept in sync with openrtb_ext.ExtStoredRequest - storedRequestId, dataType, _, err := jsonparser.Get(data, "ext", openrtb_ext.PrebidExtKey, "storedrequest", "id") + storedRequestId, dataType, _, err := getExtPrebidValue(data, "storedrequest", "id") if dataType == jsonparser.NotExist { return "", false, nil diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index d245d72b5..727b8dbea 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -1078,6 +1078,71 @@ func TestReferer(t *testing.T) { } } +func TestParseImpInfoOpenAdsAlias(t *testing.T) { + tests := []struct { + name string + input string + wantImp openrtb_ext.ExtImpPrebid + }{ + { + name: "openads only at imp.ext", + input: `{"imp":[{"id":"imp1","ext":{"openads":{"storedrequest":{"id":"42"},"options":{"echovideoattrs":true}}}}]}`, + wantImp: openrtb_ext.ExtImpPrebid{StoredRequest: &openrtb_ext.ExtStoredRequest{ID: "42"}, Options: &openrtb_ext.Options{EchoVideoAttrs: true}}, + }, + { + name: "both keys, openads wins", + input: `{"imp":[{"id":"imp1","ext":{"prebid":{"storedrequest":{"id":"FROM_PREBID"}},"openads":{"storedrequest":{"id":"FROM_OPENADS"}}}}]}`, + wantImp: openrtb_ext.ExtImpPrebid{StoredRequest: &openrtb_ext.ExtStoredRequest{ID: "FROM_OPENADS"}}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + impInfo, errs := parseImpInfo([]byte(tc.input)) + assert.Empty(t, errs, "no errors expected") + if assert.Len(t, impInfo, 1) { + assert.Equal(t, tc.wantImp, impInfo[0].ImpExtPrebid) + } + }) + } +} + +func TestGetStoredRequestIdOpenAdsAlias(t *testing.T) { + tests := []struct { + name string + input string + wantID string + wantHas bool + }{ + { + name: "openads alias", + input: `{"ext":{"openads":{"storedrequest":{"id":"sr-99"}}}}`, + wantID: "sr-99", + wantHas: true, + }, + { + name: "openads wins over prebid", + input: `{"ext":{"prebid":{"storedrequest":{"id":"FROM_PREBID"}},"openads":{"storedrequest":{"id":"FROM_OPENADS"}}}}`, + wantID: "FROM_OPENADS", + wantHas: true, + }, + { + name: "neither", + input: `{"ext":{"data":{}}}`, + wantID: "", + wantHas: false, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + id, has, err := getStoredRequestId([]byte(tc.input)) + assert.NoError(t, err) + assert.Equal(t, tc.wantHas, has) + assert.Equal(t, tc.wantID, id) + }) + } +} + func TestParseImpInfoSingleImpression(t *testing.T) { expectedRes := []ImpExtPrebidData{ diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index 16b52873d..e12a082f6 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -5828,17 +5828,26 @@ func parseRequestAliases(r openrtb2.BidRequest) (map[string]string, error) { return nil, nil } - ext := struct { - Prebid struct { - Aliases map[string]string `json:"aliases"` - } `json:"prebid"` - }{} - + // Top-level alias resolution: if ext.openads is present it is used + // in full; ext.prebid is ignored entirely in that case. + var ext map[string]json.RawMessage if err := jsonutil.Unmarshal(r.Ext, &ext); err != nil { return nil, err } - - return ext.Prebid.Aliases, nil + src, ok := ext[openrtb_ext.OpenAdsExtKey] + if !ok { + src, ok = ext[openrtb_ext.PrebidExtKey] + } + if !ok { + return nil, nil + } + var pe struct { + Aliases map[string]string `json:"aliases"` + } + if err := jsonutil.Unmarshal(src, &pe); err != nil { + return nil, err + } + return pe.Aliases, nil } func getInfoFromImp(req *openrtb_ext.RequestWrapper) (json.RawMessage, string, error) { @@ -5851,9 +5860,14 @@ func getInfoFromImp(req *openrtb_ext.RequestWrapper) (json.RawMessage, string, e return nil, "", err } + // Top-level alias resolution: openads wins outright when present. + prebidJSON := bidderExts[openrtb_ext.OpenAdsExtKey] + if prebidJSON == nil { + prebidJSON = bidderExts[openrtb_ext.PrebidExtKey] + } var extPrebid openrtb_ext.ExtImpPrebid - if bidderExts[openrtb_ext.PrebidExtKey] != nil { - if err := jsonutil.UnmarshalValid(bidderExts[openrtb_ext.PrebidExtKey], &extPrebid); err != nil { + if prebidJSON != nil { + if err := jsonutil.UnmarshalValid(prebidJSON, &extPrebid); err != nil { return nil, "", err } } diff --git a/exchange/exchangetest/aliases-openads-key.json b/exchange/exchangetest/aliases-openads-key.json new file mode 100644 index 000000000..ca6b631f1 --- /dev/null +++ b/exchange/exchangetest/aliases-openads-key.json @@ -0,0 +1,125 @@ +{ + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "openads": { + "bidder": { + "appnexus": { + "placementId": 1 + }, + "districtm": { + "placementId": 2 + } + } + } + } + } + ], + "ext": { + "openads": { + "aliases": { + "districtm": "appnexus" + } + } + } + }, + "usersyncs": { + "appnexus": "123" + } + }, + "outgoingRequests": { + "appnexus": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "user": { + "buyeruid": "123" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "bidder": { + "placementId": 1 + } + } + } + ] + } + }, + "mockResponse": { + "errors": [ + "appnexus-error" + ] + } + }, + "districtm": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "user": { + "buyeruid": "123" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "bidder": { + "placementId": 2 + } + } + } + ] + } + }, + "mockResponse": { + "errors": [ + "districtm-error" + ] + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "ext": { + "errors": { + "appnexus": [ + "appnexus-error" + ], + "districtm": [ + "districtm-error" + ] + } + } + } + } +} diff --git a/exchange/exchangetest/passthrough_root_and_imp_openads_key.json b/exchange/exchangetest/passthrough_root_and_imp_openads_key.json new file mode 100644 index 000000000..8114c5517 --- /dev/null +++ b/exchange/exchangetest/passthrough_root_and_imp_openads_key.json @@ -0,0 +1,130 @@ +{ + "passthrough_flag": true, + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "openads": { + "bidder": { + "appnexus": { + "placementId": 1 + } + }, + "passthrough": { + "imp_passthrough_val": 20 + } + } + } + } + ], + "ext": { + "openads": { + "passthrough": { + "bid_response_passthrough": 20 + } + } + } + } + }, + "outgoingRequests": { + "appnexus": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "bidder": { + "placementId": 1 + } + } + } + ] + } + }, + "mockResponse": { + "pbsSeatBids": [ + { + "pbsBids": [ + { + "ortbBid": { + "id": "apn-bid", + "impid": "my-imp-id", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "ext": { + "someField": "someValue", + "origbidcpm": 0.3 + } + }, + "bidType": "video" + } + ], + "seat": "appnexus" + } + ] + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "seatbid": [ + { + "seat": "appnexus", + "bid": [ + { + "id": "apn-bid", + "impid": "my-imp-id", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "ext": { + "someField": "someValue", + "origbidcpm": 0.3, + "prebid": { + "meta": { + }, + "type": "video", + "passthrough": { + "imp_passthrough_val": 20 + } + } + } + } + ] + } + ] + }, + "ext": { + "prebid": { + "passthrough": { + "bid_response_passthrough": 20 + } + } + } + } +} diff --git a/exchange/utils.go b/exchange/utils.go index 7606cd424..b8ae38b3a 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -660,7 +660,13 @@ func splitImps(imps []openrtb2.Imp, requestValidator ortb.RequestValidator, requ } var impExtPrebid map[string]json.RawMessage - if impExtPrebidJSON, exists := impExt[openrtb_ext.PrebidExtKey]; exists { + // Top-level alias resolution: when imp.ext.openads is present it is + // used in full; imp.ext.prebid is ignored entirely in that case. + impExtPrebidJSON, exists := impExt[openrtb_ext.OpenAdsExtKey] + if !exists { + impExtPrebidJSON, exists = impExt[openrtb_ext.PrebidExtKey] + } + if exists { // validation already performed by impExt unmarshal. no error is possible here, proven by tests. jsonutil.Unmarshal(impExtPrebidJSON, &impExtPrebid) } @@ -731,7 +737,8 @@ var allowedImpExtPrebidFields = map[string]interface{}{ } var deniedImpExtFields = map[string]interface{}{ - openrtb_ext.PrebidExtKey: struct{}{}, + openrtb_ext.PrebidExtKey: struct{}{}, + openrtb_ext.OpenAdsExtKey: struct{}{}, } func createSanitizedImpExt(impExt, impExtPrebid map[string]json.RawMessage) (map[string]json.RawMessage, error) { diff --git a/exchange/utils_test.go b/exchange/utils_test.go index 1139c2bda..98d6f7af0 100644 --- a/exchange/utils_test.go +++ b/exchange/utils_test.go @@ -303,6 +303,30 @@ func TestSplitImps(t *testing.T) { expectedImps: nil, expectedError: "merging bidder imp first party data for imp imp1 results in an invalid imp: [some error]", }, + { + description: "openads alias for prebid", + givenImps: []openrtb2.Imp{ + {ID: "imp1", Ext: json.RawMessage(`{"openads":{"bidder":{"bidderA":{"imp1ParamA":"imp1ValueA"}}}}`)}, + }, + expectedImps: map[string][]openrtb2.Imp{ + "bidderA": { + {ID: "imp1", Ext: json.RawMessage(`{"bidder":{"imp1ParamA":"imp1ValueA"}}`)}, + }, + }, + expectedError: "", + }, + { + description: "openads alias, both keys present, openads wins", + givenImps: []openrtb2.Imp{ + {ID: "imp1", Ext: json.RawMessage(`{"prebid":{"bidder":{"bidderA":{"fromPrebid":true}}},"openads":{"bidder":{"bidderB":{"fromOpenAds":true}}}}`)}, + }, + expectedImps: map[string][]openrtb2.Imp{ + "bidderB": { + {ID: "imp1", Ext: json.RawMessage(`{"bidder":{"fromOpenAds":true}}`)}, + }, + }, + expectedError: "", + }, } for _, test := range testCases { @@ -875,7 +899,7 @@ func TestExtractAdapterReqBidderParamsMap(t *testing.T) { name: "malformed req.ext", givenBidRequest: &openrtb2.BidRequest{Ext: json.RawMessage("malformed")}, want: nil, - wantErr: errors.New("error decoding Request.ext : expect { or n, but found m"), + wantErr: errors.New("error decoding Request.ext"), }, { name: "extract bidder params from req.Ext for input request in adapter code", @@ -887,7 +911,11 @@ func TestExtractAdapterReqBidderParamsMap(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := ExtractReqExtBidderParamsMap(tt.givenBidRequest) - assert.Equal(t, tt.wantErr, err, "err") + if tt.wantErr == nil { + assert.NoError(t, err, "err") + } else { + assert.ErrorContains(t, err, tt.wantErr.Error(), "err") + } assert.Equal(t, tt.want, got, "result") }) } diff --git a/openrtb_ext/device.go b/openrtb_ext/device.go index 2107a3081..b8e4cd483 100644 --- a/openrtb_ext/device.go +++ b/openrtb_ext/device.go @@ -12,6 +12,10 @@ import ( // PrebidExtKey represents the prebid extension key used in requests const PrebidExtKey = "prebid" +// OpenAdsExtKey is an alias for PrebidExtKey accepted on inbound request.ext +// and imp.ext. Outbound requests still use PrebidExtKey. +const OpenAdsExtKey = "openads" + // PrebidExtBidderKey represents the field name within request.imp.ext.prebid reserved for bidder params. const PrebidExtBidderKey = "bidder" diff --git a/openrtb_ext/floors.go b/openrtb_ext/floors.go index 1faa62923..c0d335f66 100644 --- a/openrtb_ext/floors.go +++ b/openrtb_ext/floors.go @@ -1,9 +1,11 @@ package openrtb_ext import ( + "encoding/json" "maps" "slices" + "github.com/prebid/prebid-server/v3/util/jsonutil" "github.com/prebid/prebid-server/v3/util/ptrutil" ) @@ -150,6 +152,33 @@ type ExtImp struct { Prebid *ImpExtPrebid `json:"prebid,omitempty"` } +// UnmarshalJSON resolves the prebid sub-object at the top level: if +// "openads" is present it is used in full, otherwise "prebid" is used. +// "prebid" is never consulted when "openads" exists, even for fields +// missing from openads. Outbound marshaling continues to emit "prebid". +func (e *ExtImp) UnmarshalJSON(data []byte) error { + type alias ExtImp + aux := &struct { + Prebid *json.RawMessage `json:"prebid,omitempty"` + OpenAds *json.RawMessage `json:"openads,omitempty"` + *alias + }{ + alias: (*alias)(e), + } + if err := jsonutil.Unmarshal(data, aux); err != nil { + return err + } + src := aux.OpenAds + if src == nil { + src = aux.Prebid + } + if src == nil { + return nil + } + e.Prebid = &ImpExtPrebid{} + return jsonutil.Unmarshal(*src, e.Prebid) +} + type ImpExtPrebid struct { Floors Price `json:"floors,omitempty"` } diff --git a/openrtb_ext/floors_test.go b/openrtb_ext/floors_test.go index df7d1e5dc..b436ef088 100644 --- a/openrtb_ext/floors_test.go +++ b/openrtb_ext/floors_test.go @@ -1,9 +1,11 @@ package openrtb_ext import ( + "encoding/json" "reflect" "testing" + "github.com/prebid/prebid-server/v3/util/jsonutil" "github.com/prebid/prebid-server/v3/util/ptrutil" "github.com/stretchr/testify/assert" ) @@ -480,3 +482,60 @@ func TestFloorRuleDeepCopyNil(t *testing.T) { t.Errorf("PriceFloorRules.DeepCopy() = %v, want %v", got, nil) } } + +func TestExtImpUnmarshalOpenAdsAlias(t *testing.T) { + tests := []struct { + name string + input string + wantNil bool + wantFloor float64 + }{ + { + name: "prebid key only", + input: `{"prebid":{"floors":{"floormin":1.5}}}`, + wantFloor: 1.5, + }, + { + name: "openads key only", + input: `{"openads":{"floors":{"floormin":2.5}}}`, + wantFloor: 2.5, + }, + { + name: "both keys, openads wins", + input: `{"prebid":{"floors":{"floormin":1}},"openads":{"floors":{"floormin":99}}}`, + wantFloor: 99, + }, + { + name: "neither key", + input: `{"bidder":{"x":1}}`, + wantNil: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var got ExtImp + err := jsonutil.UnmarshalValid([]byte(tc.input), &got) + assert.NoError(t, err) + if tc.wantNil { + assert.Nil(t, got.Prebid) + return + } + if assert.NotNil(t, got.Prebid) { + assert.Equal(t, tc.wantFloor, got.Prebid.Floors.FloorMin) + } + }) + } +} + +func TestExtImpMarshalEmitsPrebid(t *testing.T) { + in := []byte(`{"openads":{"floors":{"floormin":3}}}`) + var ext ExtImp + err := jsonutil.UnmarshalValid(in, &ext) + assert.NoError(t, err) + + out, err := json.Marshal(&ext) + assert.NoError(t, err) + assert.Contains(t, string(out), `"prebid"`) + assert.NotContains(t, string(out), `"openads"`) +} diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go index 7f8978e6e..292e1bcbc 100644 --- a/openrtb_ext/request.go +++ b/openrtb_ext/request.go @@ -40,6 +40,33 @@ type ExtRequest struct { SChain *openrtb2.SupplyChain `json:"schain,omitempty"` } +// UnmarshalJSON resolves the prebid configuration at the top level: if +// "openads" is present it is used in full, otherwise "prebid" is used. +// "prebid" is never consulted when "openads" exists, even for fields +// missing from openads. Unknown fields inside the chosen block are +// ignored. Outbound marshaling continues to emit "prebid". +func (e *ExtRequest) UnmarshalJSON(data []byte) error { + type alias ExtRequest + aux := &struct { + Prebid *json.RawMessage `json:"prebid,omitempty"` + OpenAds *json.RawMessage `json:"openads,omitempty"` + *alias + }{ + alias: (*alias)(e), + } + if err := jsonutil.Unmarshal(data, aux); err != nil { + return err + } + src := aux.OpenAds + if src == nil { + src = aux.Prebid + } + if src == nil { + return nil + } + return jsonutil.Unmarshal(*src, &e.Prebid) +} + // ExtRequestPrebid defines the contract for bidrequest.ext.prebid type ExtRequestPrebid struct { AdServerTargeting []AdServerTarget `json:"adservertargeting,omitempty"` diff --git a/openrtb_ext/request_test.go b/openrtb_ext/request_test.go index 92f148db4..9c6214640 100644 --- a/openrtb_ext/request_test.go +++ b/openrtb_ext/request_test.go @@ -777,3 +777,65 @@ func TestCloneExtRequestPrebid(t *testing.T) { } } + +func TestExtRequestUnmarshalOpenAdsAlias(t *testing.T) { + tests := []struct { + name string + input string + wantDebug bool + wantChannel *ExtRequestPrebidChannel + }{ + { + name: "prebid key only", + input: `{"prebid":{"debug":true,"channel":{"name":"pbjs","version":"v10"}}}`, + wantDebug: true, + wantChannel: &ExtRequestPrebidChannel{Name: "pbjs", Version: "v10"}, + }, + { + name: "openads key only", + input: `{"openads":{"debug":true,"channel":{"name":"pbjs","version":"v10"}}}`, + wantDebug: true, + wantChannel: &ExtRequestPrebidChannel{Name: "pbjs", Version: "v10"}, + }, + { + name: "both keys, openads wins", + input: `{"prebid":{"debug":true},"openads":{"debug":false}}`, + wantDebug: false, + wantChannel: nil, + }, + { + name: "openads with signing-style fields ignored", + input: `{"openads":{"debug":true,"version":"sig-v1","intSigs":["a","b"]}}`, + wantDebug: true, + wantChannel: nil, + }, + { + name: "neither key", + input: `{"schain":{"complete":1,"nodes":[],"ver":"1.0"}}`, + wantDebug: false, + wantChannel: nil, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var got ExtRequest + err := jsonutil.UnmarshalValid([]byte(tc.input), &got) + assert.NoError(t, err) + assert.Equal(t, tc.wantDebug, got.Prebid.Debug, "Prebid.Debug") + assert.Equal(t, tc.wantChannel, got.Prebid.Channel, "Prebid.Channel") + }) + } +} + +func TestExtRequestMarshalEmitsPrebid(t *testing.T) { + in := []byte(`{"openads":{"debug":true}}`) + var ext ExtRequest + err := jsonutil.UnmarshalValid(in, &ext) + assert.NoError(t, err) + + out, err := json.Marshal(&ext) + assert.NoError(t, err) + assert.Contains(t, string(out), `"prebid"`) + assert.NotContains(t, string(out), `"openads"`) +} diff --git a/openrtb_ext/request_wrapper.go b/openrtb_ext/request_wrapper.go index 7b73d3186..5cfad1b40 100644 --- a/openrtb_ext/request_wrapper.go +++ b/openrtb_ext/request_wrapper.go @@ -772,7 +772,16 @@ func (re *RequestExt) unmarshal(extJson json.RawMessage) error { return err } - prebidJson, hasPrebid := re.ext[prebidKey] + // Top-level alias resolution: when ext.openads is present it is used + // in full and ext.prebid is ignored entirely. The chosen block is + // stored under prebidKey so outbound marshaling emits "prebid". + prebidJson, hasPrebid := re.ext[OpenAdsExtKey] + if hasPrebid { + re.ext[prebidKey] = prebidJson + } else { + prebidJson, hasPrebid = re.ext[prebidKey] + } + delete(re.ext, OpenAdsExtKey) if hasPrebid { re.prebid = &ExtRequestPrebid{} } @@ -1709,7 +1718,16 @@ func (e *ImpExt) unmarshal(extJson json.RawMessage) error { return err } - prebidJson, hasPrebid := e.ext[prebidKey] + // Top-level alias resolution: when imp.ext.openads is present it is + // used in full and imp.ext.prebid is ignored entirely. The chosen + // block is stored under prebidKey so outbound marshaling emits "prebid". + prebidJson, hasPrebid := e.ext[OpenAdsExtKey] + if hasPrebid { + e.ext[prebidKey] = prebidJson + } else { + prebidJson, hasPrebid = e.ext[prebidKey] + } + delete(e.ext, OpenAdsExtKey) if hasPrebid { e.prebid = &ExtImpPrebid{} } diff --git a/openrtb_ext/request_wrapper_test.go b/openrtb_ext/request_wrapper_test.go index e4c14b603..4e17d4fbb 100644 --- a/openrtb_ext/request_wrapper_test.go +++ b/openrtb_ext/request_wrapper_test.go @@ -2476,3 +2476,68 @@ func TestRegExtGetGPCSetGPC(t *testing.T) { assert.Equal(t, regExtGPC, gpc) assert.NotSame(t, regExtGPC, gpc) } + +func TestRequestExtOpenAdsAlias(t *testing.T) { + t.Run("openads decodes into prebid and marshals as prebid", func(t *testing.T) { + re := &RequestExt{} + err := re.unmarshal([]byte(`{"openads":{"debug":true,"channel":{"name":"pbjs","version":"v10"}}}`)) + assert.NoError(t, err) + + got := re.GetPrebid() + if assert.NotNil(t, got) { + assert.True(t, got.Debug) + if assert.NotNil(t, got.Channel) { + assert.Equal(t, "pbjs", got.Channel.Name) + } + } + + // Re-marshal: outbound must use "prebid", not "openads". + re.SetPrebid(re.GetPrebid()) + out, err := re.marshal() + assert.NoError(t, err) + assert.Contains(t, string(out), `"prebid"`) + assert.NotContains(t, string(out), `"openads"`) + }) + + t.Run("both keys present, openads wins", func(t *testing.T) { + re := &RequestExt{} + err := re.unmarshal([]byte(`{"prebid":{"debug":true},"openads":{"debug":false}}`)) + assert.NoError(t, err) + assert.False(t, re.GetPrebid().Debug) + + re.SetPrebid(re.GetPrebid()) + out, err := re.marshal() + assert.NoError(t, err) + assert.NotContains(t, string(out), `"openads"`) + }) +} + +func TestImpExtOpenAdsAlias(t *testing.T) { + t.Run("openads decodes into prebid and marshals as prebid", func(t *testing.T) { + ie := &ImpExt{} + err := ie.unmarshal([]byte(`{"openads":{"storedrequest":{"id":"sr-7"}}}`)) + assert.NoError(t, err) + got := ie.GetPrebid() + if assert.NotNil(t, got) && assert.NotNil(t, got.StoredRequest) { + assert.Equal(t, "sr-7", got.StoredRequest.ID) + } + + ie.SetPrebid(ie.GetPrebid()) + out, err := ie.marshal() + assert.NoError(t, err) + assert.Contains(t, string(out), `"prebid"`) + assert.NotContains(t, string(out), `"openads"`) + }) + + t.Run("both keys present, openads wins", func(t *testing.T) { + ie := &ImpExt{} + err := ie.unmarshal([]byte(`{"prebid":{"storedrequest":{"id":"FROM_PREBID"}},"openads":{"storedrequest":{"id":"FROM_OPENADS"}}}`)) + assert.NoError(t, err) + assert.Equal(t, "FROM_OPENADS", ie.GetPrebid().StoredRequest.ID) + + ie.SetPrebid(ie.GetPrebid()) + out, err := ie.marshal() + assert.NoError(t, err) + assert.NotContains(t, string(out), `"openads"`) + }) +} From 9c4c098b5bbd6e0493756e4b131269880401ea7a Mon Sep 17 00:00:00 2001 From: "mike.hoyt" Date: Mon, 4 May 2026 17:10:15 -0600 Subject: [PATCH 2/4] Fix a few tests --- exchange/exchangetest/aliases-openads-key.json | 9 ++++++++- modules/openads/signatures/module_test.go | 14 +++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/exchange/exchangetest/aliases-openads-key.json b/exchange/exchangetest/aliases-openads-key.json index ca6b631f1..7ab96c3eb 100644 --- a/exchange/exchangetest/aliases-openads-key.json +++ b/exchange/exchangetest/aliases-openads-key.json @@ -97,7 +97,14 @@ } } } - ] + ], + "ext": { + "prebid": { + "aliases": { + "districtm": "appnexus" + } + } + } } }, "mockResponse": { diff --git a/modules/openads/signatures/module_test.go b/modules/openads/signatures/module_test.go index 512e11a64..06f59a67b 100644 --- a/modules/openads/signatures/module_test.go +++ b/modules/openads/signatures/module_test.go @@ -148,10 +148,10 @@ func TestBuilder(t *testing.T) { func TestHandleBidderRequestHook_Success(t *testing.T) { tests := []struct { - name string - initialExt json.RawMessage + name string + initialExt json.RawMessage mockResponse []SignatureWrapper - expectedSig Signature + expectedSig Signature }{ { name: "add int_sigs to nil ext", @@ -188,7 +188,7 @@ func TestHandleBidderRequestHook_Success(t *testing.T) { }, { name: "replace openads with int_sigs", - initialExt: json.RawMessage(`{"openads": 1, "prebid": {"debug": true}}`), + initialExt: json.RawMessage(`{"openads": {"version": 0, "int_sigs": []}, "prebid": {"debug": true}}`), mockResponse: []SignatureWrapper{ {Name: "testbidder", SIS: Signature{Envelope: "envelope-3", Source: "source-3"}}, }, @@ -658,7 +658,7 @@ func TestHandleBidderRequestHook_MutationTracking(t *testing.T) { assert.Equal(t, SchemaVersion, openadsExt.Version) require.Len(t, openadsExt.IntSigs, 1) - + expectedSig := Signature{ Envelope: "test-signature", Source: "test-source", @@ -832,7 +832,7 @@ func TestTCPIntegration(t *testing.T) { assert.Equal(t, SchemaVersion, openadsExt.Version) require.Len(t, openadsExt.IntSigs, 1) - + expectedSig := Signature{ Envelope: "sig-1", Source: "source-1", @@ -938,7 +938,7 @@ func TestUDSIntegration(t *testing.T) { assert.Equal(t, SchemaVersion, openadsExt.Version) require.Len(t, openadsExt.IntSigs, 1) - + expectedSig := Signature{ Envelope: "sig-1", Source: "source-1", From 034444d8ea6e22671708df8ba3d0b2aae1503586 Mon Sep 17 00:00:00 2001 From: "mike.hoyt" Date: Thu, 7 May 2026 12:22:11 -0600 Subject: [PATCH 3/4] mho-OPATH-6429-Allow-OpenAds-Ext Remove floors parsing as it isn't necessary --- .../exchangetest/aliases-openads-key.json | 38 ++++++------ .../passthrough_root_and_imp_openads_key.json | 18 +++--- openrtb_ext/floors.go | 28 --------- openrtb_ext/floors_test.go | 58 ------------------- 4 files changed, 31 insertions(+), 111 deletions(-) diff --git a/exchange/exchangetest/aliases-openads-key.json b/exchange/exchangetest/aliases-openads-key.json index 7ab96c3eb..fdb5a7527 100644 --- a/exchange/exchangetest/aliases-openads-key.json +++ b/exchange/exchangetest/aliases-openads-key.json @@ -16,11 +16,13 @@ "ext": { "openads": { "bidder": { - "appnexus": { - "placementId": 1 + "thetradedesk": { + "publisherId": "pub1", + "supplySourceId": "src1" }, - "districtm": { - "placementId": 2 + "ttdalias": { + "publisherId": "pub2", + "supplySourceId": "src2" } } } @@ -30,17 +32,17 @@ "ext": { "openads": { "aliases": { - "districtm": "appnexus" + "ttdalias": "thetradedesk" } } } }, "usersyncs": { - "appnexus": "123" + "thetradedesk": "123" } }, "outgoingRequests": { - "appnexus": { + "thetradedesk": { "expectRequest": { "ortbRequest": { "id": "some-request-id", @@ -60,7 +62,8 @@ }, "ext": { "bidder": { - "placementId": 1 + "publisherId": "pub1", + "supplySourceId": "src1" } } } @@ -69,11 +72,11 @@ }, "mockResponse": { "errors": [ - "appnexus-error" + "thetradedesk-error" ] } }, - "districtm": { + "ttdalias": { "expectRequest": { "ortbRequest": { "id": "some-request-id", @@ -93,7 +96,8 @@ }, "ext": { "bidder": { - "placementId": 2 + "publisherId": "pub2", + "supplySourceId": "src2" } } } @@ -101,7 +105,7 @@ "ext": { "prebid": { "aliases": { - "districtm": "appnexus" + "ttdalias": "thetradedesk" } } } @@ -109,7 +113,7 @@ }, "mockResponse": { "errors": [ - "districtm-error" + "ttdalias-error" ] } } @@ -119,11 +123,11 @@ "id": "some-request-id", "ext": { "errors": { - "appnexus": [ - "appnexus-error" + "thetradedesk": [ + "thetradedesk-error" ], - "districtm": [ - "districtm-error" + "ttdalias": [ + "ttdalias-error" ] } } diff --git a/exchange/exchangetest/passthrough_root_and_imp_openads_key.json b/exchange/exchangetest/passthrough_root_and_imp_openads_key.json index 8114c5517..61670cb6b 100644 --- a/exchange/exchangetest/passthrough_root_and_imp_openads_key.json +++ b/exchange/exchangetest/passthrough_root_and_imp_openads_key.json @@ -17,8 +17,9 @@ "ext": { "openads": { "bidder": { - "appnexus": { - "placementId": 1 + "thetradedesk": { + "publisherId": "pub1", + "supplySourceId": "src1" } }, "passthrough": { @@ -38,7 +39,7 @@ } }, "outgoingRequests": { - "appnexus": { + "thetradedesk": { "expectRequest": { "ortbRequest": { "id": "some-request-id", @@ -55,7 +56,8 @@ }, "ext": { "bidder": { - "placementId": 1 + "publisherId": "pub1", + "supplySourceId": "src1" } } } @@ -68,7 +70,7 @@ "pbsBids": [ { "ortbBid": { - "id": "apn-bid", + "id": "ttd-bid", "impid": "my-imp-id", "price": 0.3, "w": 200, @@ -82,7 +84,7 @@ "bidType": "video" } ], - "seat": "appnexus" + "seat": "thetradedesk" } ] } @@ -93,10 +95,10 @@ "id": "some-request-id", "seatbid": [ { - "seat": "appnexus", + "seat": "thetradedesk", "bid": [ { - "id": "apn-bid", + "id": "ttd-bid", "impid": "my-imp-id", "price": 0.3, "w": 200, diff --git a/openrtb_ext/floors.go b/openrtb_ext/floors.go index c0d335f66..2a71bff2c 100644 --- a/openrtb_ext/floors.go +++ b/openrtb_ext/floors.go @@ -1,11 +1,9 @@ package openrtb_ext import ( - "encoding/json" "maps" "slices" - "github.com/prebid/prebid-server/v3/util/jsonutil" "github.com/prebid/prebid-server/v3/util/ptrutil" ) @@ -152,32 +150,6 @@ type ExtImp struct { Prebid *ImpExtPrebid `json:"prebid,omitempty"` } -// UnmarshalJSON resolves the prebid sub-object at the top level: if -// "openads" is present it is used in full, otherwise "prebid" is used. -// "prebid" is never consulted when "openads" exists, even for fields -// missing from openads. Outbound marshaling continues to emit "prebid". -func (e *ExtImp) UnmarshalJSON(data []byte) error { - type alias ExtImp - aux := &struct { - Prebid *json.RawMessage `json:"prebid,omitempty"` - OpenAds *json.RawMessage `json:"openads,omitempty"` - *alias - }{ - alias: (*alias)(e), - } - if err := jsonutil.Unmarshal(data, aux); err != nil { - return err - } - src := aux.OpenAds - if src == nil { - src = aux.Prebid - } - if src == nil { - return nil - } - e.Prebid = &ImpExtPrebid{} - return jsonutil.Unmarshal(*src, e.Prebid) -} type ImpExtPrebid struct { Floors Price `json:"floors,omitempty"` diff --git a/openrtb_ext/floors_test.go b/openrtb_ext/floors_test.go index b436ef088..f17267e93 100644 --- a/openrtb_ext/floors_test.go +++ b/openrtb_ext/floors_test.go @@ -1,11 +1,9 @@ package openrtb_ext import ( - "encoding/json" "reflect" "testing" - "github.com/prebid/prebid-server/v3/util/jsonutil" "github.com/prebid/prebid-server/v3/util/ptrutil" "github.com/stretchr/testify/assert" ) @@ -483,59 +481,3 @@ func TestFloorRuleDeepCopyNil(t *testing.T) { } } -func TestExtImpUnmarshalOpenAdsAlias(t *testing.T) { - tests := []struct { - name string - input string - wantNil bool - wantFloor float64 - }{ - { - name: "prebid key only", - input: `{"prebid":{"floors":{"floormin":1.5}}}`, - wantFloor: 1.5, - }, - { - name: "openads key only", - input: `{"openads":{"floors":{"floormin":2.5}}}`, - wantFloor: 2.5, - }, - { - name: "both keys, openads wins", - input: `{"prebid":{"floors":{"floormin":1}},"openads":{"floors":{"floormin":99}}}`, - wantFloor: 99, - }, - { - name: "neither key", - input: `{"bidder":{"x":1}}`, - wantNil: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var got ExtImp - err := jsonutil.UnmarshalValid([]byte(tc.input), &got) - assert.NoError(t, err) - if tc.wantNil { - assert.Nil(t, got.Prebid) - return - } - if assert.NotNil(t, got.Prebid) { - assert.Equal(t, tc.wantFloor, got.Prebid.Floors.FloorMin) - } - }) - } -} - -func TestExtImpMarshalEmitsPrebid(t *testing.T) { - in := []byte(`{"openads":{"floors":{"floormin":3}}}`) - var ext ExtImp - err := jsonutil.UnmarshalValid(in, &ext) - assert.NoError(t, err) - - out, err := json.Marshal(&ext) - assert.NoError(t, err) - assert.Contains(t, string(out), `"prebid"`) - assert.NotContains(t, string(out), `"openads"`) -} From ac0ecb5839e3692b32cc3b5449b3e8baa6b434cb Mon Sep 17 00:00:00 2001 From: "mike.hoyt" Date: Thu, 7 May 2026 12:32:38 -0600 Subject: [PATCH 4/4] mho-OPATH-6429-Allow-OpenAds-Ext Remove added newlines --- openrtb_ext/floors.go | 1 - openrtb_ext/floors_test.go | 1 - 2 files changed, 2 deletions(-) diff --git a/openrtb_ext/floors.go b/openrtb_ext/floors.go index 2a71bff2c..1faa62923 100644 --- a/openrtb_ext/floors.go +++ b/openrtb_ext/floors.go @@ -150,7 +150,6 @@ type ExtImp struct { Prebid *ImpExtPrebid `json:"prebid,omitempty"` } - type ImpExtPrebid struct { Floors Price `json:"floors,omitempty"` } diff --git a/openrtb_ext/floors_test.go b/openrtb_ext/floors_test.go index f17267e93..df7d1e5dc 100644 --- a/openrtb_ext/floors_test.go +++ b/openrtb_ext/floors_test.go @@ -480,4 +480,3 @@ func TestFloorRuleDeepCopyNil(t *testing.T) { t.Errorf("PriceFloorRules.DeepCopy() = %v, want %v", got, nil) } } -