Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions src/adapters/bidderFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,11 +279,11 @@ export function newBidder<B extends BidderCode>(spec: BidderSpec<B>) {
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)
Expand Down Expand Up @@ -345,7 +345,10 @@ export function newBidder<B extends BidderCode>(spec: BidderSpec<B>) {
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);
Expand Down Expand Up @@ -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]));
Expand All @@ -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;
Expand Down
12 changes: 12 additions & 0 deletions src/auction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
6 changes: 5 additions & 1 deletion test/spec/auctionmanager_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1159,7 +1163,7 @@ describe('auctionmanager.js', function () {
bids[0],
{
bidderCode: BIDDER_CODE,
mediaType: 'video-outstream',
mediaType: 'video',
}
);
spec.interpretResponse.returns(bids1);
Expand Down
108 changes: 108 additions & 0 deletions test/spec/unit/core/bidderFactory_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Loading