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  }