From 5dffd987385b0227e85f8fa59b854f9f6f41c89c Mon Sep 17 00:00:00 2001 From: ym-ainiguez Date: Wed, 29 Jul 2026 10:30:30 -0700 Subject: [PATCH] Yieldmo Bid Adapter: robustness fixes (video imp guard, floor, response body) Three defensive fixes surfaced by an adapter inspection (TDTN-8800): 1. createNewVideoBid: a response bid whose impid doesn't match a requested imp left `imp` undefined, so imp.id/imp.video.* threw and aborted interpretResponse, dropping every other bid in the response. Guard with `if (!imp) return null` and filter the null in interpretResponse, so only the bad bid is skipped. 2. getBidFloor: getFloor() can throw or return undefined (no matching rule); wrap in try/catch and default to {} so a floors misconfig can't drop the bid. 3. interpretResponse: guard a missing/null serverResponse.body (return []) instead of throwing on data.length / data.seatbid. Adds a unit test for each. Refs TDTN-8800. Co-Authored-By: Claude Opus 4.8 (1M context) --- modules/yieldmoBidAdapter.js | 28 +++++++++-- test/spec/modules/yieldmoBidAdapter_spec.js | 55 +++++++++++++++++++++ 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/modules/yieldmoBidAdapter.js b/modules/yieldmoBidAdapter.js index f606194e132..de0fbc12b9c 100644 --- a/modules/yieldmoBidAdapter.js +++ b/modules/yieldmoBidAdapter.js @@ -196,7 +196,10 @@ export const spec = { */ interpretResponse: function (serverResponse, bidRequest) { const bids = []; - const data = serverResponse.body; + const data = serverResponse && serverResponse.body; + if (!data) { + return bids; + } if (data.length > 0) { data.forEach(response => { if (response.cpm > 0) { @@ -206,7 +209,12 @@ export const spec = { } if (data.seatbid) { const seatbids = data.seatbid.reduce((acc, seatBid) => acc.concat(seatBid.bid), []); - seatbids.forEach(bid => bids.push(createNewVideoBid(bid, bidRequest))); + seatbids.forEach(bid => { + const videoBid = createNewVideoBid(bid, bidRequest); + if (videoBid) { + bids.push(videoBid); + } + }); } return bids; }, @@ -328,6 +336,14 @@ function createNewBannerBid(response) { function createNewVideoBid(response, bidRequest) { const imp = (deepAccess(bidRequest, 'data.imp') || []).find(imp => imp.id === response.impid); + // A response bid whose impid doesn't match a requested imp can't be built + // (imp.id / imp.video.* would throw). Skip it so one malformed bid doesn't + // abort interpretResponse and drop every other bid in the response; the + // caller filters out this null. + if (!imp) { + return null; + } + const result = { dealId: response.dealid, requestId: imp.id, @@ -525,7 +541,13 @@ function getBidFloor(bidRequest, mediaType) { let floorInfo = {}; if (typeof bidRequest.getFloor === 'function') { - floorInfo = bidRequest.getFloor({ currency: CURRENCY, mediaType, size: '*' }); + // getFloor can throw or return undefined (e.g. no matching floor rule); + // guard both so a floors misconfig can't drop the bid. + try { + floorInfo = bidRequest.getFloor({ currency: CURRENCY, mediaType, size: '*' }) || {}; + } catch (e) { + logError('Yieldmo: getFloor threw; falling back to params bidfloor', e); + } } return floorInfo.floor || bidRequest.params.bidfloor || bidRequest.params.bidFloor || 0; diff --git a/test/spec/modules/yieldmoBidAdapter_spec.js b/test/spec/modules/yieldmoBidAdapter_spec.js index 4f4454c17ae..3e138153132 100644 --- a/test/spec/modules/yieldmoBidAdapter_spec.js +++ b/test/spec/modules/yieldmoBidAdapter_spec.js @@ -329,6 +329,16 @@ describe('YieldmoAdapter', function () { expect(placementsData[1].bidFloor).to.equal(1.23); }); + it('should fall back to params bidFloor when getFloor throws or returns undefined (no throw)', function () { + const throwing = mockBannerBid({ getFloor: () => { throw new Error('boom'); } }, { bidFloor: 0.42 }); + expect(() => build([throwing])).to.not.throw(); + expect(JSON.parse(buildAndGetPlacementInfo([throwing]))[0].bidFloor).to.equal(0.42); + + const undef = mockBannerBid({ getFloor: () => undefined }, { bidFloor: 0.42 }); + expect(() => build([undef])).to.not.throw(); + expect(JSON.parse(buildAndGetPlacementInfo([undef]))[0].bidFloor).to.equal(0.42); + }); + it('should use bidFloor if no floors module is available', function() { const placementsData = JSON.parse(buildAndGetPlacementInfo([ mockBannerBid({}, { bidFloor: 1.2 }), @@ -920,6 +930,44 @@ describe('YieldmoAdapter', function () { }); }); + it('should skip a video bid whose impid does not match a requested imp, keeping the rest', function () { + const response = mockServerResponse(); + const seatbid = [ + { + bid: { + adm: '', + adomain: ['www.example.com'], + crid: 'unmatched-crid', + impid: 'no-such-imp', + price: 1.5, + dealid: 'YMO_456' + }, + }, + { + bid: { + adm: '', + adomain: ['www.example.com'], + crid: 'matched-crid', + impid: '91ea8bba1', + price: 2.0, + dealid: 'YMO_789' + }, + }, + ]; + const bidRequest = { + data: { imp: [{ id: '91ea8bba1', video: { h: 250, w: 300 } }] }, + }; + response.body.seatbid = seatbid; + + // The unmatched bid must be skipped (not throw), and must not drop the + // matching video bid. + const newResponse = spec.interpretResponse(response, bidRequest); + const videoBids = newResponse.filter(b => b.mediaType === 'video'); + expect(videoBids.length).to.equal(1); + expect(videoBids[0].requestId).to.equal('91ea8bba1'); + expect(videoBids[0].creativeId).to.equal('matched-crid'); + }); + it('should not add responses if the cpm is 0 or null', function () { const response = mockServerResponse(); response.body[0].cpm = 0; @@ -928,6 +976,13 @@ describe('YieldmoAdapter', function () { response.body[0].cpm = null; expect(spec.interpretResponse(response)).to.deep.equal([]); }); + + it('should return [] (not throw) when the response body is missing or null', function () { + expect(spec.interpretResponse({ body: null })).to.deep.equal([]); + expect(spec.interpretResponse({ body: undefined })).to.deep.equal([]); + expect(spec.interpretResponse({})).to.deep.equal([]); + expect(() => spec.interpretResponse(undefined)).to.not.throw(); + }); }); describe('getUserSync', function () {