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 () {