diff --git a/src/adapters/bidderFactory.ts b/src/adapters/bidderFactory.ts index bacd954f641..737c26117e1 100644 --- a/src/adapters/bidderFactory.ts +++ b/src/adapters/bidderFactory.ts @@ -279,11 +279,11 @@ export function newBidder(spec: BidderSpec) { const tidGuard = guardTids(bidderRequest); const adUnitCodesHandled = {}; - function addBidWithCode(adUnitCode: string, bid: Bid) { + function addBidWithCode(adUnitCode: string, bid: Bid, responseMediaType = null) { const metrics = useMetrics(bid.metrics); metrics.checkpoint('addBidResponse'); adUnitCodesHandled[adUnitCode] = true; - if (metrics.measureTime('addBidResponse.validate', () => isValid(adUnitCode, bid))) { + if (metrics.measureTime('addBidResponse.validate', () => isValid(adUnitCode, bid, { responseMediaType }))) { addBidResponse(adUnitCode, bid); } else { addBidResponse.reject(adUnitCode, bid, REJECTION_REASON.INVALID) @@ -345,7 +345,10 @@ export function newBidder(spec: BidderSpec) { bid.deferBilling = bidRequest.deferBilling; bid.deferRendering = bid.deferBilling && (bidResponse.deferRendering ?? typeof spec.onBidBillable !== 'function'); const prebidBid: Bid = Object.assign(createBid(bidRequest), bid, pick(bidRequest, Object.keys(TIDS))); - addBidWithCode(bidRequest.adUnitCode, prebidBid); + const responseMediaType = Object.prototype.hasOwnProperty.call(bidResponse, 'mediaType') + ? bidResponse.mediaType + : null; + addBidWithCode(bidRequest.adUnitCode, prebidBid, responseMediaType); } else { logWarn(`Bidder ${spec.code} made bid for unknown request ID: ${bidResponse.requestId}. Ignoring.`); addBidResponse.reject(null, bidResponse, REJECTION_REASON.INVALID_REQUEST_ID); @@ -647,7 +650,7 @@ function validBidSize(adUnitCode, bid: BannerBid, { index = auctionManager.index } // Validate the arguments sent to us by the adapter. If this returns false, the bid should be totally ignored. -export function isValid(adUnitCode: string, bid: Bid, { index = auctionManager.index } = {}) { +export function isValid(adUnitCode: string, bid: Bid, { index = auctionManager.index, responseMediaType = bid.mediaType } = {}) { function hasValidKeys() { const bidKeys = Object.keys(bid); return COMMON_BID_RESPONSE_KEYS.every(key => bidKeys.includes(key) && ![undefined, null].includes(bid[key])); @@ -672,6 +675,21 @@ export function isValid(adUnitCode: string, bid: Bid, { index = auctionManager.i return false; } + const auctionOptions = config.getConfig('auctionOptions') || {}; + const rejectUnknownMediaTypes = auctionOptions.rejectUnknownMediaTypes === true; + const rejectInvalidMediaTypes = auctionOptions.rejectInvalidMediaTypes !== false; + const mediaTypes = index.getMediaTypes(bid); + if (mediaTypes && Object.keys(mediaTypes).length > 0) { + if (responseMediaType == null && rejectUnknownMediaTypes) { + logError(errorMessage(`Bid mediaType is required. Allowed: ${Object.keys(mediaTypes).join(', ')}`)); + return false; + } + if (responseMediaType != null && rejectInvalidMediaTypes && !mediaTypes.hasOwnProperty(responseMediaType)) { + logError(errorMessage(`Bid mediaType '${responseMediaType}' is not supported by the ad unit. Allowed: ${Object.keys(mediaTypes).join(', ')}`)); + return false; + } + } + if (FEATURES.NATIVE && bid.mediaType === 'native' && !nativeBidIsValid(bid, { index })) { logError(errorMessage('Native bid missing some required properties.')); return false; diff --git a/src/auction.ts b/src/auction.ts index 4f805603c55..6a7a9643938 100644 --- a/src/auction.ts +++ b/src/auction.ts @@ -143,6 +143,18 @@ export interface AuctionOptionsConfig { * to pre-10.12 rendering logic. */ legacyRender?: boolean; + + /** + * When true, reject bids without a response `mediaType` when the ad unit has an explicit mediaTypes list. + * Default is false to preserve legacy behavior for responses that omit mediaType. + */ + rejectUnknownMediaTypes?: boolean; + + /** + * When true, reject bids with a response `mediaType` that does not match the ad unit's explicit mediaTypes list. + * Default is true; set to false to keep mismatched mediaType responses. + */ + rejectInvalidMediaTypes?: boolean; } export interface PriceBucketConfig { diff --git a/src/config.ts b/src/config.ts index 827856df0f0..665ce89778d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -63,7 +63,7 @@ function attachProperties(config, useDefaultValues = true) { } : {} const validateauctionOptions = (() => { - const boolKeys = ['secondaryBidders', 'suppressStaleRender', 'suppressExpiredRender', 'legacyRender']; + const boolKeys = ['suppressStaleRender', 'suppressExpiredRender', 'legacyRender', 'rejectUnknownMediaTypes', 'rejectInvalidMediaTypes']; const arrKeys = ['secondaryBidders'] const allKeys = [].concat(boolKeys).concat(arrKeys); diff --git a/test/spec/auctionmanager_spec.js b/test/spec/auctionmanager_spec.js index 90bdc857e98..58151526325 100644 --- a/test/spec/auctionmanager_spec.js +++ b/test/spec/auctionmanager_spec.js @@ -105,6 +105,10 @@ function mockBidRequest(bid, opts) { const defaultMediaType = { banner: { sizes: [[300, 250], [300, 600]] + }, + video: { + context: 'outstream', + renderer: {} } } const mediaType = (opts && opts.mediaType) ? opts.mediaType : defaultMediaType; @@ -1159,7 +1163,7 @@ describe('auctionmanager.js', function () { bids[0], { bidderCode: BIDDER_CODE, - mediaType: 'video-outstream', + mediaType: 'video', } ); spec.interpretResponse.returns(bids1); diff --git a/test/spec/unit/core/bidderFactory_spec.js b/test/spec/unit/core/bidderFactory_spec.js index 153bccab5ca..383d7441a62 100644 --- a/test/spec/unit/core/bidderFactory_spec.js +++ b/test/spec/unit/core/bidderFactory_spec.js @@ -1691,6 +1691,114 @@ describe('bidderFactory', () => { }); }); }) + + describe('media type validation', () => { + let req; + + function mkResponse(props) { + return Object.assign({ + requestId: req.bidId, + cpm: 1, + ttl: 60, + creativeId: '123', + netRevenue: true, + currency: 'USD', + width: 1, + height: 2, + mediaType: 'banner', + }, props); + } + + function checkValid(bid, opts = {}) { + return isValid('au', bid, { + index: stubAuctionIndex({ bidRequests: [req] }), + ...opts, + }); + } + + beforeEach(() => { + req = { + ...MOCK_BIDS_REQUEST.bids[0], + mediaTypes: { + banner: { + sizes: [[1, 2]] + } + } + }; + }); + + it('should reject video bid when ad unit only has banner', () => { + expect(checkValid(mkResponse({ mediaType: 'video' }))).to.be.false; + }); + + it('should accept video bid when ad unit has both banner and video', () => { + req.mediaTypes = { + banner: { sizes: [[1, 2]] }, + video: { context: 'instream' } + }; + expect(checkValid(mkResponse({ mediaType: 'video', vastUrl: 'http://vast.xml' }))).to.be.true; + }); + + it('should skip media type check when adapter omits mediaType', () => { + req.mediaTypes = { + video: { context: 'instream' } + }; + + expect(checkValid(mkResponse({ mediaType: 'banner' }), { responseMediaType: null })).to.be.true; + }); + + it('should reject unknown media type when configured and adapter omits mediaType', () => { + req.mediaTypes = { + video: { context: 'instream' } + }; + config.setConfig({ + auctionOptions: { + rejectUnknownMediaTypes: true + } + }); + + expect(checkValid(mkResponse({ mediaType: 'banner' }), { responseMediaType: null })).to.be.false; + }); + + it('should keep legacy behavior when rejectUnknownMediaTypes is disabled', () => { + req.mediaTypes = { + video: { context: 'instream' } + }; + config.setConfig({ + auctionOptions: { + rejectUnknownMediaTypes: false + } + }); + + expect(checkValid(mkResponse({ mediaType: 'banner' }), { responseMediaType: null })).to.be.true; + }); + + it('should allow mismatched media type when rejectInvalidMediaTypes is disabled', () => { + req.mediaTypes = { + banner: { sizes: [[1, 2]] } + }; + config.setConfig({ + auctionOptions: { + rejectInvalidMediaTypes: false + } + }); + + expect(checkValid(mkResponse({ mediaType: 'video' }))).to.be.true; + }); + + it('should reject mismatched media type when rejectInvalidMediaTypes is enabled', () => { + req.mediaTypes = { + banner: { sizes: [[1, 2]] } + }; + config.setConfig({ + auctionOptions: { + rejectInvalidMediaTypes: true + } + }); + + expect(checkValid(mkResponse({ mediaType: 'video' }))).to.be.false; + }); + }); }); describe('gzip compression', () => {