github.com/prebid/prebid-server/v2@v2.18.0/exchange/bidder_validate_bids.go (about) 1 package exchange 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "strings" 8 9 "github.com/prebid/openrtb/v20/openrtb2" 10 "github.com/prebid/prebid-server/v2/adapters" 11 "github.com/prebid/prebid-server/v2/currency" 12 "github.com/prebid/prebid-server/v2/exchange/entities" 13 "github.com/prebid/prebid-server/v2/experiment/adscert" 14 "github.com/prebid/prebid-server/v2/hooks/hookexecution" 15 "github.com/prebid/prebid-server/v2/openrtb_ext" 16 goCurrency "golang.org/x/text/currency" 17 ) 18 19 // addValidatedBidderMiddleware returns a bidder that removes invalid bids from the argument bidder's response. 20 // These will be converted into errors instead. 21 // 22 // The goal here is to make sure that the response contains Bids which are valid given the initial Request, 23 // so that Publishers can trust the Bids they get from Prebid Server. 24 func addValidatedBidderMiddleware(bidder AdaptedBidder) AdaptedBidder { 25 return &validatedBidder{ 26 bidder: bidder, 27 } 28 } 29 30 type validatedBidder struct { 31 bidder AdaptedBidder 32 } 33 34 func (v *validatedBidder) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, hookExecutor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) ([]*entities.PbsOrtbSeatBid, extraBidderRespInfo, []error) { 35 seatBids, extraBidderRespInfo, errs := v.bidder.requestBid(ctx, bidderRequest, conversions, reqInfo, adsCertSigner, bidRequestOptions, alternateBidderCodes, hookExecutor, ruleToAdjustments) 36 for _, seatBid := range seatBids { 37 if validationErrors := removeInvalidBids(bidderRequest.BidRequest, seatBid, bidRequestOptions.responseDebugAllowed); len(validationErrors) > 0 { 38 errs = append(errs, validationErrors...) 39 } 40 } 41 return seatBids, extraBidderRespInfo, errs 42 } 43 44 // validateBids will run some validation checks on the returned bids and excise any invalid bids 45 func removeInvalidBids(request *openrtb2.BidRequest, seatBid *entities.PbsOrtbSeatBid, debug bool) []error { 46 // Exit early if there is nothing to do. 47 if seatBid == nil || len(seatBid.Bids) == 0 { 48 return nil 49 } 50 51 // By design, default currency is USD. 52 if cerr := validateCurrency(request.Cur, seatBid.Currency); cerr != nil { 53 seatBid.Bids = nil 54 return []error{cerr} 55 } 56 57 errs := make([]error, 0, len(seatBid.Bids)) 58 validBids := make([]*entities.PbsOrtbBid, 0, len(seatBid.Bids)) 59 for _, bid := range seatBid.Bids { 60 if ok, err := validateBid(bid, debug); ok { 61 validBids = append(validBids, bid) 62 } else if err != nil { 63 errs = append(errs, err) 64 } 65 } 66 seatBid.Bids = validBids 67 return errs 68 } 69 70 // validateCurrency will run currency validation checks and return true if it passes, false otherwise. 71 func validateCurrency(requestAllowedCurrencies []string, bidCurrency string) error { 72 // Default currency is `USD` by design. 73 defaultCurrency := "USD" 74 // Make sure bid currency is a valid ISO currency code 75 if bidCurrency == "" { 76 // If bid currency is not set, then consider it's default currency. 77 bidCurrency = defaultCurrency 78 } 79 currencyUnit, cerr := goCurrency.ParseISO(bidCurrency) 80 if cerr != nil { 81 return cerr 82 } 83 // Make sure the bid currency is allowed from bid request via `cur` field. 84 // If `cur` field array from bid request is empty, then consider it accepts the default currency. 85 currencyAllowed := false 86 if len(requestAllowedCurrencies) == 0 { 87 requestAllowedCurrencies = []string{defaultCurrency} 88 } 89 for _, allowedCurrency := range requestAllowedCurrencies { 90 if strings.ToUpper(allowedCurrency) == currencyUnit.String() { 91 currencyAllowed = true 92 break 93 } 94 } 95 if !currencyAllowed { 96 return fmt.Errorf( 97 "Bid currency is not allowed. Was '%s', wants: ['%s']", 98 currencyUnit.String(), 99 strings.Join(requestAllowedCurrencies, "', '"), 100 ) 101 } 102 103 return nil 104 } 105 106 // validateBid will run the supplied bid through validation checks and return true if it passes, false otherwise. 107 func validateBid(bid *entities.PbsOrtbBid, debug bool) (bool, error) { 108 if bid.Bid == nil { 109 return false, errors.New("Empty bid object submitted.") 110 } 111 112 if bid.Bid.ID == "" { 113 return false, errors.New("Bid missing required field 'id'") 114 } 115 if bid.Bid.ImpID == "" { 116 return false, fmt.Errorf("Bid \"%s\" missing required field 'impid'", bid.Bid.ID) 117 } 118 if bid.Bid.Price < 0.0 { 119 if debug { 120 return false, fmt.Errorf("Bid \"%s\" does not contain a positive (or zero if there is a deal) 'price'", bid.Bid.ID) 121 } 122 return false, nil 123 } 124 if bid.Bid.Price == 0.0 && bid.Bid.DealID == "" { 125 if debug { 126 return false, fmt.Errorf("Bid \"%s\" does not contain positive 'price' which is required since there is no deal set for this bid", bid.Bid.ID) 127 } 128 return false, nil 129 } 130 if bid.Bid.CrID == "" { 131 return false, fmt.Errorf("Bid \"%s\" missing creative ID", bid.Bid.ID) 132 } 133 134 return true, nil 135 }