github.com/prebid/prebid-server@v0.275.0/endpoints/openrtb2/video_auction.go (about)

     1  package openrtb2
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"net/url"
    11  	"regexp"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/buger/jsonparser"
    17  	"github.com/gofrs/uuid"
    18  	"github.com/golang/glog"
    19  	"github.com/julienschmidt/httprouter"
    20  	"github.com/prebid/openrtb/v19/openrtb2"
    21  	"github.com/prebid/prebid-server/hooks"
    22  	"github.com/prebid/prebid-server/hooks/hookexecution"
    23  	"github.com/prebid/prebid-server/ortb"
    24  	"github.com/prebid/prebid-server/privacy"
    25  	jsonpatch "gopkg.in/evanphx/json-patch.v4"
    26  
    27  	accountService "github.com/prebid/prebid-server/account"
    28  	"github.com/prebid/prebid-server/analytics"
    29  	"github.com/prebid/prebid-server/config"
    30  	"github.com/prebid/prebid-server/errortypes"
    31  	"github.com/prebid/prebid-server/exchange"
    32  	"github.com/prebid/prebid-server/metrics"
    33  	"github.com/prebid/prebid-server/openrtb_ext"
    34  	"github.com/prebid/prebid-server/prebid_cache_client"
    35  	"github.com/prebid/prebid-server/stored_requests"
    36  	"github.com/prebid/prebid-server/stored_requests/backends/empty_fetcher"
    37  	"github.com/prebid/prebid-server/usersync"
    38  	"github.com/prebid/prebid-server/util/iputil"
    39  	"github.com/prebid/prebid-server/util/ptrutil"
    40  	"github.com/prebid/prebid-server/util/uuidutil"
    41  	"github.com/prebid/prebid-server/version"
    42  )
    43  
    44  var defaultRequestTimeout int64 = 5000
    45  
    46  func NewVideoEndpoint(
    47  	uuidGenerator uuidutil.UUIDGenerator,
    48  	ex exchange.Exchange,
    49  	validator openrtb_ext.BidderParamValidator,
    50  	requestsById stored_requests.Fetcher,
    51  	videoFetcher stored_requests.Fetcher,
    52  	accounts stored_requests.AccountFetcher,
    53  	cfg *config.Configuration,
    54  	met metrics.MetricsEngine,
    55  	pbsAnalytics analytics.PBSAnalyticsModule,
    56  	disabledBidders map[string]string,
    57  	defReqJSON []byte,
    58  	bidderMap map[string]openrtb_ext.BidderName,
    59  	cache prebid_cache_client.Client,
    60  	tmaxAdjustments *exchange.TmaxAdjustmentsPreprocessed,
    61  ) (httprouter.Handle, error) {
    62  
    63  	if ex == nil || validator == nil || requestsById == nil || accounts == nil || cfg == nil || met == nil {
    64  		return nil, errors.New("NewVideoEndpoint requires non-nil arguments.")
    65  	}
    66  
    67  	defRequest := len(defReqJSON) > 0
    68  
    69  	ipValidator := iputil.PublicNetworkIPValidator{
    70  		IPv4PrivateNetworks: cfg.RequestValidation.IPv4PrivateNetworksParsed,
    71  		IPv6PrivateNetworks: cfg.RequestValidation.IPv6PrivateNetworksParsed,
    72  	}
    73  
    74  	videoEndpointRegexp := regexp.MustCompile(`[<>]`)
    75  
    76  	return httprouter.Handle((&endpointDeps{
    77  		uuidGenerator,
    78  		ex,
    79  		validator,
    80  		requestsById,
    81  		videoFetcher,
    82  		accounts,
    83  		cfg,
    84  		met,
    85  		pbsAnalytics,
    86  		disabledBidders,
    87  		defRequest,
    88  		defReqJSON,
    89  		bidderMap,
    90  		cache,
    91  		videoEndpointRegexp,
    92  		ipValidator,
    93  		empty_fetcher.EmptyFetcher{},
    94  		hooks.EmptyPlanBuilder{},
    95  		tmaxAdjustments}).VideoAuctionEndpoint), nil
    96  }
    97  
    98  /*
    99   1. Parse "storedrequestid" field from simplified endpoint request body.
   100   2. If config flag to require that field is set (which it will be for us) and this field is not given then error out here.
   101   3. Load the stored request JSON for the given storedrequestid, if the id was invalid then error out here.
   102   4. Use "json-patch" 3rd party library to merge the request body JSON data into the stored request JSON data.
   103   5. Unmarshal the merged JSON data into a Go structure.
   104   6. Add fields from merged JSON data that correspond to an OpenRTB request into the OpenRTB bid request we are building.
   105      a. Unmarshal certain OpenRTB defined structs directly into the OpenRTB bid request.
   106      b. In cases where customized logic is needed just copy/fill the fields in directly.
   107   7. Call setFieldsImplicitly from auction.go to get basic data from the HTTP request into an OpenRTB bid request to start building the OpenRTB bid request.
   108   8. Loop through ad pods to build array of Imps into OpenRTB request, for each pod:
   109      a. Load the stored impression to use as the basis for impressions generated for this pod from the configid field.
   110      b. NumImps = adpoddurationsec / MIN_VALUE(allowedDurations)
   111      c. Build impression array for this pod:
   112      I.Create array of NumImps entries initialized to the base impression loaded from the configid.
   113   1. If requireexactdurations = true, iterate over allowdDurations and for (NumImps / len(allowedDurations)) number of Imps set minduration = maxduration = allowedDurations[i]
   114   2. If requireexactdurations = false, set maxduration = MAX_VALUE(allowedDurations)
   115      II. Set Imp.id field to "podX_Y" where X is the pod index and Y is the impression index within this pod.
   116      d. Append impressions for this pod to the overall list of impressions in the OpenRTB bid request.
   117   9. Call validateRequest() function from auction.go to validate the generated request.
   118   10. Call HoldAuction() function to run the auction for the OpenRTB bid request that was built in the previous step.
   119   11. Build proper response format.
   120  */
   121  func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
   122  	start := time.Now()
   123  
   124  	vo := analytics.VideoObject{
   125  		Status:    http.StatusOK,
   126  		Errors:    make([]error, 0),
   127  		StartTime: start,
   128  	}
   129  
   130  	labels := metrics.Labels{
   131  		Source:        metrics.DemandUnknown,
   132  		RType:         metrics.ReqTypeVideo,
   133  		PubID:         metrics.PublisherUnknown,
   134  		CookieFlag:    metrics.CookieFlagUnknown,
   135  		RequestStatus: metrics.RequestStatusOK,
   136  	}
   137  
   138  	debugQuery := r.URL.Query().Get("debug")
   139  	cacheTTL := int64(3600)
   140  	if deps.cfg.CacheURL.DefaultTTLs.Video > 0 {
   141  		cacheTTL = int64(deps.cfg.CacheURL.DefaultTTLs.Video)
   142  	}
   143  	debugLog := exchange.DebugLog{
   144  		Enabled:       strings.EqualFold(debugQuery, "true"),
   145  		CacheType:     prebid_cache_client.TypeXML,
   146  		TTL:           cacheTTL,
   147  		Regexp:        deps.debugLogRegexp,
   148  		DebugOverride: exchange.IsDebugOverrideEnabled(r.Header.Get(exchange.DebugOverrideHeader), deps.cfg.Debug.OverrideToken),
   149  	}
   150  	debugLog.DebugEnabledOrOverridden = debugLog.Enabled || debugLog.DebugOverride
   151  
   152  	defer func() {
   153  		if len(debugLog.CacheKey) > 0 && vo.VideoResponse == nil {
   154  			err := debugLog.PutDebugLogError(deps.cache, deps.cfg.CacheURL.ExpectedTimeMillis, vo.Errors)
   155  			if err != nil {
   156  				vo.Errors = append(vo.Errors, err)
   157  			}
   158  		}
   159  		deps.metricsEngine.RecordRequest(labels)
   160  		deps.metricsEngine.RecordRequestTime(labels, time.Since(start))
   161  		deps.analytics.LogVideoObject(&vo)
   162  	}()
   163  
   164  	w.Header().Set("X-Prebid", version.BuildXPrebidHeader(version.Ver))
   165  
   166  	lr := &io.LimitedReader{
   167  		R: r.Body,
   168  		N: deps.cfg.MaxRequestSize,
   169  	}
   170  	requestJson, err := io.ReadAll(lr)
   171  	if err != nil {
   172  		handleError(&labels, w, []error{err}, &vo, &debugLog)
   173  		return
   174  	}
   175  
   176  	resolvedRequest := requestJson
   177  	if debugLog.DebugEnabledOrOverridden {
   178  		debugLog.Data.Request = string(requestJson)
   179  		if headerBytes, err := json.Marshal(r.Header); err == nil {
   180  			debugLog.Data.Headers = string(headerBytes)
   181  		} else {
   182  			debugLog.Data.Headers = fmt.Sprintf("Unable to marshal headers data: %s", err.Error())
   183  		}
   184  	}
   185  
   186  	//load additional data - stored simplified req
   187  	storedRequestId, err := getVideoStoredRequestId(requestJson)
   188  
   189  	if err != nil {
   190  		if deps.cfg.VideoStoredRequestRequired {
   191  			handleError(&labels, w, []error{err}, &vo, &debugLog)
   192  			return
   193  		}
   194  	} else {
   195  		storedRequest, errs := deps.loadStoredVideoRequest(context.Background(), storedRequestId)
   196  		if len(errs) > 0 {
   197  			handleError(&labels, w, errs, &vo, &debugLog)
   198  			return
   199  		}
   200  
   201  		//merge incoming req with stored video req
   202  		resolvedRequest, err = jsonpatch.MergePatch(storedRequest, requestJson)
   203  		if err != nil {
   204  			handleError(&labels, w, []error{err}, &vo, &debugLog)
   205  			return
   206  		}
   207  	}
   208  	//unmarshal and validate combined result
   209  	videoBidReq, errL, podErrors := deps.parseVideoRequest(resolvedRequest, r.Header)
   210  	if len(errL) > 0 {
   211  		handleError(&labels, w, errL, &vo, &debugLog)
   212  		return
   213  	}
   214  
   215  	vo.VideoRequest = videoBidReq
   216  
   217  	var bidReq = &openrtb2.BidRequest{}
   218  	if deps.defaultRequest {
   219  		if err := json.Unmarshal(deps.defReqJSON, bidReq); err != nil {
   220  			err = fmt.Errorf("Invalid JSON in Default Request Settings: %s", err)
   221  			handleError(&labels, w, []error{err}, &vo, &debugLog)
   222  			return
   223  		}
   224  	}
   225  
   226  	//create full open rtb req from full video request
   227  	mergeData(videoBidReq, bidReq)
   228  	// If debug query param is set, force the response to enable test flag
   229  	if debugLog.DebugEnabledOrOverridden {
   230  		bidReq.Test = 1
   231  	}
   232  
   233  	initialPodNumber := len(videoBidReq.PodConfig.Pods)
   234  	if len(podErrors) > 0 {
   235  		//remove incorrect pods
   236  		videoBidReq = cleanupVideoBidRequest(videoBidReq, podErrors)
   237  	}
   238  
   239  	//create impressions array
   240  	imps, podErrors := deps.createImpressions(videoBidReq, podErrors)
   241  
   242  	if len(podErrors) == initialPodNumber {
   243  		resPodErr := make([]string, 0)
   244  		for _, podEr := range podErrors {
   245  			resPodErr = append(resPodErr, strings.Join(podEr.ErrMsgs, ", "))
   246  		}
   247  		err := errors.New(fmt.Sprintf("all pods are incorrect: %s", strings.Join(resPodErr, "; ")))
   248  		errL = append(errL, err)
   249  		handleError(&labels, w, errL, &vo, &debugLog)
   250  		return
   251  	}
   252  
   253  	bidReq.Imp = imps
   254  	bidReq.ID = "bid_id" //TODO: look at prebid.js
   255  
   256  	// all code after this line should use the bidReqWrapper instead of bidReq directly
   257  	bidReqWrapper := &openrtb_ext.RequestWrapper{BidRequest: bidReq}
   258  
   259  	// Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers).
   260  	deps.setFieldsImplicitly(r, bidReqWrapper)
   261  
   262  	if err := ortb.SetDefaults(bidReqWrapper); err != nil {
   263  		handleError(&labels, w, errL, &vo, &debugLog)
   264  		return
   265  	}
   266  
   267  	errL = deps.validateRequest(bidReqWrapper, false, false, nil, false)
   268  	if errortypes.ContainsFatalError(errL) {
   269  		handleError(&labels, w, errL, &vo, &debugLog)
   270  		return
   271  	}
   272  
   273  	ctx := context.Background()
   274  	timeout := deps.cfg.AuctionTimeouts.LimitAuctionTimeout(time.Duration(bidReqWrapper.TMax) * time.Millisecond)
   275  	if timeout > 0 {
   276  		var cancel context.CancelFunc
   277  		ctx, cancel = context.WithDeadline(ctx, start.Add(timeout))
   278  		defer cancel()
   279  	}
   280  
   281  	// Read Usersyncs/Cookie
   282  	decoder := usersync.Base64Decoder{}
   283  	usersyncs := usersync.ReadCookie(r, decoder, &deps.cfg.HostCookie)
   284  	usersync.SyncHostCookie(r, usersyncs, &deps.cfg.HostCookie)
   285  
   286  	if bidReqWrapper.App != nil {
   287  		labels.Source = metrics.DemandApp
   288  		labels.PubID = getAccountID(bidReqWrapper.App.Publisher)
   289  	} else { // both bidReqWrapper.App == nil and bidReqWrapper.Site != nil are true
   290  		labels.Source = metrics.DemandWeb
   291  		if usersyncs.HasAnyLiveSyncs() {
   292  			labels.CookieFlag = metrics.CookieFlagYes
   293  		} else {
   294  			labels.CookieFlag = metrics.CookieFlagNo
   295  		}
   296  		labels.PubID = getAccountID(bidReqWrapper.Site.Publisher)
   297  	}
   298  
   299  	// Look up account now that we have resolved the pubID value
   300  	account, acctIDErrs := accountService.GetAccount(ctx, deps.cfg, deps.accounts, labels.PubID, deps.metricsEngine)
   301  	if len(acctIDErrs) > 0 {
   302  		handleError(&labels, w, acctIDErrs, &vo, &debugLog)
   303  		return
   304  	}
   305  
   306  	activityControl := privacy.NewActivityControl(&account.Privacy)
   307  
   308  	secGPC := r.Header.Get("Sec-GPC")
   309  	auctionRequest := &exchange.AuctionRequest{
   310  		BidRequestWrapper:          bidReqWrapper,
   311  		Account:                    *account,
   312  		UserSyncs:                  usersyncs,
   313  		RequestType:                labels.RType,
   314  		StartTime:                  start,
   315  		LegacyLabels:               labels,
   316  		GlobalPrivacyControlHeader: secGPC,
   317  		PubID:                      labels.PubID,
   318  		HookExecutor:               hookexecution.EmptyHookExecutor{},
   319  		TmaxAdjustments:            deps.tmaxAdjustments,
   320  		Activities:                 activityControl,
   321  	}
   322  
   323  	auctionResponse, err := deps.ex.HoldAuction(ctx, auctionRequest, &debugLog)
   324  	defer func() {
   325  		if !auctionRequest.BidderResponseStartTime.IsZero() {
   326  			deps.metricsEngine.RecordOverheadTime(metrics.MakeAuctionResponse, time.Since(auctionRequest.BidderResponseStartTime))
   327  		}
   328  	}()
   329  	vo.RequestWrapper = bidReqWrapper
   330  	var response *openrtb2.BidResponse
   331  	if auctionResponse != nil {
   332  		response = auctionResponse.BidResponse
   333  	}
   334  	vo.Response = response
   335  	vo.SeatNonBid = auctionResponse.GetSeatNonBid()
   336  	if err != nil {
   337  		errL := []error{err}
   338  		handleError(&labels, w, errL, &vo, &debugLog)
   339  		return
   340  	}
   341  
   342  	//build simplified response
   343  	bidResp, err := buildVideoResponse(response, podErrors)
   344  	if err != nil {
   345  		errL := []error{err}
   346  		handleError(&labels, w, errL, &vo, &debugLog)
   347  		return
   348  	}
   349  	if bidReq.Test == 1 {
   350  		err = setSeatNonBidRaw(bidReqWrapper, auctionResponse)
   351  		if err != nil {
   352  			glog.Errorf("Error setting seat non-bid: %v", err)
   353  		}
   354  		bidResp.Ext = response.Ext
   355  	}
   356  
   357  	if len(bidResp.AdPods) == 0 && debugLog.DebugEnabledOrOverridden {
   358  		err := debugLog.PutDebugLogError(deps.cache, deps.cfg.CacheURL.ExpectedTimeMillis, vo.Errors)
   359  		if err != nil {
   360  			vo.Errors = append(vo.Errors, err)
   361  		} else {
   362  			bidResp.AdPods = append(bidResp.AdPods, &openrtb_ext.AdPod{
   363  				Targeting: []openrtb_ext.VideoTargeting{
   364  					{
   365  						HbCacheID: debugLog.CacheKey,
   366  					},
   367  				},
   368  			})
   369  		}
   370  	}
   371  
   372  	vo.VideoResponse = bidResp
   373  
   374  	resp, err := json.Marshal(bidResp)
   375  	//resp, err := json.Marshal(response)
   376  	if err != nil {
   377  		errL := []error{err}
   378  		handleError(&labels, w, errL, &vo, &debugLog)
   379  		return
   380  	}
   381  
   382  	w.Header().Set("Content-Type", "application/json")
   383  	w.Write(resp)
   384  
   385  }
   386  
   387  func cleanupVideoBidRequest(videoReq *openrtb_ext.BidRequestVideo, podErrors []PodError) *openrtb_ext.BidRequestVideo {
   388  	for i := len(podErrors) - 1; i >= 0; i-- {
   389  		videoReq.PodConfig.Pods = append(videoReq.PodConfig.Pods[:podErrors[i].PodIndex], videoReq.PodConfig.Pods[podErrors[i].PodIndex+1:]...)
   390  	}
   391  	return videoReq
   392  }
   393  
   394  func handleError(labels *metrics.Labels, w http.ResponseWriter, errL []error, vo *analytics.VideoObject, debugLog *exchange.DebugLog) {
   395  	if debugLog != nil && debugLog.DebugEnabledOrOverridden {
   396  		if rawUUID, err := uuid.NewV4(); err == nil {
   397  			debugLog.CacheKey = rawUUID.String()
   398  		}
   399  		errL = append(errL, fmt.Errorf("[Debug cache ID: %s]", debugLog.CacheKey))
   400  	}
   401  	labels.RequestStatus = metrics.RequestStatusErr
   402  	var errors string
   403  	var status int = http.StatusInternalServerError
   404  	for _, er := range errL {
   405  		erVal := errortypes.ReadCode(er)
   406  		if erVal == errortypes.BlacklistedAppErrorCode || erVal == errortypes.BlacklistedAcctErrorCode {
   407  			status = http.StatusServiceUnavailable
   408  			labels.RequestStatus = metrics.RequestStatusBlacklisted
   409  			break
   410  		} else if erVal == errortypes.AcctRequiredErrorCode {
   411  			status = http.StatusBadRequest
   412  			labels.RequestStatus = metrics.RequestStatusBadInput
   413  			break
   414  		} else if erVal == errortypes.MalformedAcctErrorCode {
   415  			status = http.StatusInternalServerError
   416  			labels.RequestStatus = metrics.RequestStatusAccountConfigErr
   417  			break
   418  		}
   419  		errors = fmt.Sprintf("%s %s", errors, er.Error())
   420  	}
   421  	w.WriteHeader(status)
   422  	vo.Status = status
   423  	fmt.Fprintf(w, "Critical error while running the video endpoint: %v", errors)
   424  	glog.Errorf("/openrtb2/video Critical error: %v", errors)
   425  	vo.Errors = append(vo.Errors, errL...)
   426  }
   427  
   428  func (deps *endpointDeps) createImpressions(videoReq *openrtb_ext.BidRequestVideo, podErrors []PodError) ([]openrtb2.Imp, []PodError) {
   429  	videoDur := videoReq.PodConfig.DurationRangeSec
   430  	minDuration, maxDuration := minMax(videoDur)
   431  	reqExactDur := videoReq.PodConfig.RequireExactDuration
   432  	videoData := videoReq.Video
   433  
   434  	finalImpsArray := make([]openrtb2.Imp, 0)
   435  	for ind, pod := range videoReq.PodConfig.Pods {
   436  
   437  		//load stored impression
   438  		storedImpressionId := string(pod.ConfigId)
   439  		storedImp, errs := deps.loadStoredImp(storedImpressionId)
   440  		if errs != nil {
   441  			err := fmt.Sprintf("unable to load configid %s, Pod id: %d", storedImpressionId, pod.PodId)
   442  			podErr := PodError{}
   443  			podErr.PodId = pod.PodId
   444  			podErr.PodIndex = ind
   445  			podErr.ErrMsgs = append(podErr.ErrMsgs, err)
   446  			podErrors = append(podErrors, podErr)
   447  			continue
   448  		}
   449  
   450  		numImps := pod.AdPodDurationSec / minDuration
   451  		if reqExactDur {
   452  			// In case of impressions number is less than durations array, we bump up impressions number up to duration array size
   453  			// with this handler we will have one impression per specified duration
   454  			numImps = max(numImps, len(videoDur))
   455  		}
   456  		impDivNumber := numImps / len(videoDur)
   457  
   458  		impsArray := make([]openrtb2.Imp, numImps)
   459  		for impInd := range impsArray {
   460  			newImp := createImpressionTemplate(storedImp, videoData)
   461  			impsArray[impInd] = newImp
   462  			if reqExactDur {
   463  				//floor := int(math.Floor(ind/impDivNumber))
   464  				durationIndex := impInd / impDivNumber
   465  				if durationIndex > len(videoDur)-1 {
   466  					durationIndex = len(videoDur) - 1
   467  				}
   468  				impsArray[impInd].Video.MaxDuration = int64(videoDur[durationIndex])
   469  				impsArray[impInd].Video.MinDuration = int64(videoDur[durationIndex])
   470  				//fmt.Println("Imp ind  ", impInd, "duration ", videoDur[durationIndex])
   471  			} else {
   472  				impsArray[impInd].Video.MaxDuration = int64(maxDuration)
   473  			}
   474  
   475  			impsArray[impInd].ID = fmt.Sprintf("%d_%d", pod.PodId, impInd)
   476  		}
   477  		finalImpsArray = append(finalImpsArray, impsArray...)
   478  
   479  	}
   480  	return finalImpsArray, podErrors
   481  }
   482  
   483  func max(a, b int) int {
   484  	if a > b {
   485  		return a
   486  	}
   487  	return b
   488  }
   489  
   490  func createImpressionTemplate(imp openrtb2.Imp, video *openrtb2.Video) openrtb2.Imp {
   491  	//for every new impression we need to have it's own copy of video object, because we customize it in further processing
   492  	newVideo := *video
   493  	imp.Video = &newVideo
   494  	return imp
   495  }
   496  
   497  func (deps *endpointDeps) loadStoredImp(storedImpId string) (openrtb2.Imp, []error) {
   498  	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(storedRequestTimeoutMillis)*time.Millisecond)
   499  	defer cancel()
   500  
   501  	impr := openrtb2.Imp{}
   502  	_, imp, err := deps.storedReqFetcher.FetchRequests(ctx, []string{}, []string{storedImpId})
   503  	if err != nil {
   504  		return impr, err
   505  	}
   506  
   507  	if err := json.Unmarshal(imp[storedImpId], &impr); err != nil {
   508  		return impr, []error{err}
   509  	}
   510  	return impr, nil
   511  }
   512  
   513  func minMax(array []int) (int, int) {
   514  	var max = array[0]
   515  	var min = array[0]
   516  	for _, value := range array {
   517  		if max < value {
   518  			max = value
   519  		}
   520  		if min > value {
   521  			min = value
   522  		}
   523  	}
   524  	return min, max
   525  }
   526  
   527  func buildVideoResponse(bidresponse *openrtb2.BidResponse, podErrors []PodError) (*openrtb_ext.BidResponseVideo, error) {
   528  
   529  	adPods := make([]*openrtb_ext.AdPod, 0)
   530  	anyBidsReturned := false
   531  	for _, seatBid := range bidresponse.SeatBid {
   532  		for _, bid := range seatBid.Bid {
   533  			anyBidsReturned = true
   534  
   535  			var tempRespBidExt openrtb_ext.ExtBid
   536  			if err := json.Unmarshal(bid.Ext, &tempRespBidExt); err != nil {
   537  				return nil, err
   538  			}
   539  			if tempRespBidExt.Prebid.Targeting[formatTargetingKey(openrtb_ext.HbVastCacheKey, seatBid.Seat)] == "" {
   540  				continue
   541  			}
   542  
   543  			impId := bid.ImpID
   544  			podNum := strings.Split(impId, "_")[0]
   545  			podId, _ := strconv.ParseInt(podNum, 0, 64)
   546  
   547  			videoTargeting := openrtb_ext.VideoTargeting{
   548  				HbPb:       tempRespBidExt.Prebid.Targeting[formatTargetingKey(openrtb_ext.HbpbConstantKey, seatBid.Seat)],
   549  				HbPbCatDur: tempRespBidExt.Prebid.Targeting[formatTargetingKey(openrtb_ext.HbCategoryDurationKey, seatBid.Seat)],
   550  				HbCacheID:  tempRespBidExt.Prebid.Targeting[formatTargetingKey(openrtb_ext.HbVastCacheKey, seatBid.Seat)],
   551  				HbDeal:     tempRespBidExt.Prebid.Targeting[formatTargetingKey(openrtb_ext.HbDealIDConstantKey, seatBid.Seat)],
   552  			}
   553  
   554  			adPod := findAdPod(podId, adPods)
   555  			if adPod == nil {
   556  				adPod = &openrtb_ext.AdPod{
   557  					PodId:     podId,
   558  					Targeting: make([]openrtb_ext.VideoTargeting, 0, 0),
   559  				}
   560  				adPods = append(adPods, adPod)
   561  			}
   562  			adPod.Targeting = append(adPod.Targeting, videoTargeting)
   563  
   564  		}
   565  	}
   566  
   567  	//check if there are any bids in response.
   568  	//if there are no bids - empty response should be returned, no cache errors
   569  	if len(adPods) == 0 && anyBidsReturned {
   570  		//means there is a global cache error, we need to reject all bids
   571  		err := errors.New("caching failed for all bids")
   572  		return nil, err
   573  	}
   574  
   575  	// If there were incorrect pods, we put them back to response with error message
   576  	if len(podErrors) > 0 {
   577  		for _, podEr := range podErrors {
   578  			adPodEr := &openrtb_ext.AdPod{
   579  				PodId:  int64(podEr.PodId),
   580  				Errors: podEr.ErrMsgs,
   581  			}
   582  			adPods = append(adPods, adPodEr)
   583  		}
   584  	}
   585  
   586  	return &openrtb_ext.BidResponseVideo{AdPods: adPods}, nil
   587  }
   588  
   589  func formatTargetingKey(key openrtb_ext.TargetingKey, bidderName string) string {
   590  	fullKey := fmt.Sprintf("%s_%s", string(key), bidderName)
   591  	if len(fullKey) > exchange.MaxKeyLength {
   592  		return string(fullKey[0:exchange.MaxKeyLength])
   593  	}
   594  	return fullKey
   595  }
   596  
   597  func findAdPod(podInd int64, pods []*openrtb_ext.AdPod) *openrtb_ext.AdPod {
   598  	for _, pod := range pods {
   599  		if pod.PodId == podInd {
   600  			return pod
   601  		}
   602  	}
   603  	return nil
   604  }
   605  
   606  func (deps *endpointDeps) loadStoredVideoRequest(ctx context.Context, storedRequestId string) ([]byte, []error) {
   607  	storedRequests, _, errs := deps.videoFetcher.FetchRequests(ctx, []string{storedRequestId}, []string{})
   608  	jsonString := storedRequests[storedRequestId]
   609  	return jsonString, errs
   610  }
   611  
   612  func getVideoStoredRequestId(request []byte) (string, error) {
   613  	value, dataType, _, err := jsonparser.Get(request, "storedrequestid")
   614  	if dataType != jsonparser.String || err != nil {
   615  		return "", &errortypes.BadInput{Message: "Unable to find required stored request id"}
   616  	}
   617  	return string(value), nil
   618  }
   619  
   620  func mergeData(videoRequest *openrtb_ext.BidRequestVideo, bidRequest *openrtb2.BidRequest) error {
   621  	if videoRequest.Site != nil {
   622  		bidRequest.Site = videoRequest.Site
   623  		if &videoRequest.Content != nil {
   624  			bidRequest.Site.Content = &videoRequest.Content
   625  		}
   626  	}
   627  
   628  	if videoRequest.App != nil {
   629  		bidRequest.App = videoRequest.App
   630  		if &videoRequest.Content != nil {
   631  			bidRequest.App.Content = &videoRequest.Content
   632  		}
   633  	}
   634  
   635  	if &videoRequest.Device != nil {
   636  		bidRequest.Device = &videoRequest.Device
   637  	}
   638  
   639  	bidRequest.User = videoRequest.User
   640  
   641  	if len(videoRequest.BCat) != 0 {
   642  		bidRequest.BCat = videoRequest.BCat
   643  	}
   644  
   645  	if len(videoRequest.BAdv) != 0 {
   646  		bidRequest.BAdv = videoRequest.BAdv
   647  	}
   648  
   649  	bidExt, err := createBidExtension(videoRequest)
   650  	if err != nil {
   651  		return err
   652  	}
   653  	if len(bidExt) > 0 {
   654  		bidRequest.Ext = bidExt
   655  	}
   656  
   657  	bidRequest.Test = videoRequest.Test
   658  
   659  	if videoRequest.TMax == 0 {
   660  		bidRequest.TMax = defaultRequestTimeout
   661  	} else {
   662  		bidRequest.TMax = videoRequest.TMax
   663  	}
   664  
   665  	if videoRequest.Regs != nil {
   666  		bidRequest.Regs = videoRequest.Regs
   667  	}
   668  
   669  	return nil
   670  }
   671  
   672  func createBidExtension(videoRequest *openrtb_ext.BidRequestVideo) ([]byte, error) {
   673  	var inclBrandCat *openrtb_ext.ExtIncludeBrandCategory
   674  	if videoRequest.IncludeBrandCategory != nil {
   675  		inclBrandCat = &openrtb_ext.ExtIncludeBrandCategory{
   676  			PrimaryAdServer:     videoRequest.IncludeBrandCategory.PrimaryAdserver,
   677  			Publisher:           videoRequest.IncludeBrandCategory.Publisher,
   678  			WithCategory:        true,
   679  			TranslateCategories: videoRequest.IncludeBrandCategory.TranslateCategories,
   680  		}
   681  	} else {
   682  		inclBrandCat = &openrtb_ext.ExtIncludeBrandCategory{
   683  			WithCategory: false,
   684  		}
   685  	}
   686  
   687  	var durationRangeSec []int
   688  	if !videoRequest.PodConfig.RequireExactDuration {
   689  		durationRangeSec = videoRequest.PodConfig.DurationRangeSec
   690  	}
   691  
   692  	targeting := openrtb_ext.ExtRequestTargeting{
   693  		PriceGranularity:     videoRequest.PriceGranularity,
   694  		IncludeBrandCategory: inclBrandCat,
   695  		DurationRangeSec:     durationRangeSec,
   696  		IncludeBidderKeys:    ptrutil.ToPtr(true),
   697  		AppendBidderNames:    videoRequest.AppendBidderNames,
   698  	}
   699  
   700  	vastXml := openrtb_ext.ExtRequestPrebidCacheVAST{}
   701  	cache := openrtb_ext.ExtRequestPrebidCache{
   702  		VastXML: &vastXml,
   703  	}
   704  
   705  	prebid := openrtb_ext.ExtRequestPrebid{
   706  		Cache:        &cache,
   707  		Targeting:    &targeting,
   708  		SupportDeals: videoRequest.SupportDeals,
   709  	}
   710  	extReq := openrtb_ext.ExtRequest{Prebid: prebid}
   711  
   712  	return json.Marshal(extReq)
   713  }
   714  
   715  func (deps *endpointDeps) parseVideoRequest(request []byte, headers http.Header) (req *openrtb_ext.BidRequestVideo, errs []error, podErrors []PodError) {
   716  	req = &openrtb_ext.BidRequestVideo{}
   717  
   718  	if err := json.Unmarshal(request, &req); err != nil {
   719  		errs = []error{err}
   720  		return
   721  	}
   722  
   723  	//if Device.UA is not present in request body, init it with user-agent from request header if it's present
   724  	if req.Device.UA == "" {
   725  		ua := headers.Get("User-Agent")
   726  
   727  		//Check UA is encoded. Without it the `+` character would get changed to a space if not actually encoded
   728  		if strings.ContainsAny(ua, "%") {
   729  			var err error
   730  			req.Device.UA, err = url.QueryUnescape(ua)
   731  			if err != nil {
   732  				req.Device.UA = ua
   733  			}
   734  		} else {
   735  			req.Device.UA = ua
   736  		}
   737  	}
   738  
   739  	errL, podErrors := deps.validateVideoRequest(req)
   740  	if len(errL) > 0 {
   741  		errs = append(errs, errL...)
   742  	}
   743  	return
   744  }
   745  
   746  type PodError struct {
   747  	PodId    int
   748  	PodIndex int
   749  	ErrMsgs  []string
   750  }
   751  
   752  func (deps *endpointDeps) validateVideoRequest(req *openrtb_ext.BidRequestVideo) ([]error, []PodError) {
   753  	errL := []error{}
   754  
   755  	if deps.cfg.VideoStoredRequestRequired && req.StoredRequestId == "" {
   756  		err := errors.New("request missing required field: storedrequestid")
   757  		errL = append(errL, err)
   758  	}
   759  	if len(req.PodConfig.DurationRangeSec) == 0 {
   760  		err := errors.New("request missing required field: PodConfig.DurationRangeSec")
   761  		errL = append(errL, err)
   762  	}
   763  	if isZeroOrNegativeDuration(req.PodConfig.DurationRangeSec) {
   764  		err := errors.New("duration array cannot contain negative or zero values")
   765  		errL = append(errL, err)
   766  	}
   767  	if len(req.PodConfig.Pods) == 0 {
   768  		err := errors.New("request missing required field: PodConfig.Pods")
   769  		errL = append(errL, err)
   770  	}
   771  	podErrors := make([]PodError, 0, 0)
   772  	podIdsSet := make(map[int]bool)
   773  	for ind, pod := range req.PodConfig.Pods {
   774  		podErr := PodError{}
   775  
   776  		if podIdsSet[pod.PodId] {
   777  			err := fmt.Sprintf("request duplicated required field: PodConfig.Pods.PodId, Pod id: %d", pod.PodId)
   778  			podErr.ErrMsgs = append(podErr.ErrMsgs, err)
   779  		} else {
   780  			podIdsSet[pod.PodId] = true
   781  		}
   782  		if pod.PodId <= 0 {
   783  			err := fmt.Sprintf("request missing required field: PodConfig.Pods.PodId, Pod index: %d", ind)
   784  			podErr.ErrMsgs = append(podErr.ErrMsgs, err)
   785  		}
   786  		if pod.AdPodDurationSec == 0 {
   787  			err := fmt.Sprintf("request missing or incorrect required field: PodConfig.Pods.AdPodDurationSec, Pod index: %d", ind)
   788  			podErr.ErrMsgs = append(podErr.ErrMsgs, err)
   789  		}
   790  		if pod.AdPodDurationSec < 0 {
   791  			err := fmt.Sprintf("request incorrect required field: PodConfig.Pods.AdPodDurationSec is negative, Pod index: %d", ind)
   792  			podErr.ErrMsgs = append(podErr.ErrMsgs, err)
   793  		}
   794  		if pod.ConfigId == "" {
   795  			err := fmt.Sprintf("request missing or incorrect required field: PodConfig.Pods.ConfigId, Pod index: %d", ind)
   796  			podErr.ErrMsgs = append(podErr.ErrMsgs, err)
   797  		}
   798  		if len(podErr.ErrMsgs) > 0 {
   799  			podErr.PodId = pod.PodId
   800  			podErr.PodIndex = ind
   801  			podErrors = append(podErrors, podErr)
   802  		}
   803  	}
   804  	if req.App == nil && req.Site == nil {
   805  		err := errors.New("request missing required field: site or app")
   806  		errL = append(errL, err)
   807  	} else if req.App != nil && req.Site != nil {
   808  		err := errors.New("request.site or request.app must be defined, but not both")
   809  		errL = append(errL, err)
   810  	} else if req.Site != nil && req.Site.ID == "" && req.Site.Page == "" {
   811  		err := errors.New("request.site missing required field: id or page")
   812  		errL = append(errL, err)
   813  	} else if req.App != nil {
   814  		if req.App.ID != "" {
   815  			if _, found := deps.cfg.BlacklistedAppMap[req.App.ID]; found {
   816  				err := &errortypes.BlacklistedApp{Message: fmt.Sprintf("Prebid-server does not process requests from App ID: %s", req.App.ID)}
   817  				errL = append(errL, err)
   818  				return errL, podErrors
   819  			}
   820  		} else {
   821  			if req.App.Bundle == "" {
   822  				err := errors.New("request.app missing required field: id or bundle")
   823  				errL = append(errL, err)
   824  			}
   825  		}
   826  	}
   827  
   828  	if req.Video != nil {
   829  		if len(req.Video.MIMEs) == 0 {
   830  			err := errors.New("request missing required field: Video.Mimes")
   831  			errL = append(errL, err)
   832  		} else {
   833  			mimes := make([]string, 0, len(req.Video.MIMEs))
   834  			for _, mime := range req.Video.MIMEs {
   835  				if mime != "" {
   836  					mimes = append(mimes, mime)
   837  				}
   838  			}
   839  			if len(mimes) == 0 {
   840  				err := errors.New("request missing required field: Video.Mimes, mime types contains empty strings only")
   841  				errL = append(errL, err)
   842  			}
   843  			req.Video.MIMEs = mimes
   844  		}
   845  
   846  		if len(req.Video.Protocols) == 0 {
   847  			err := errors.New("request missing required field: Video.Protocols")
   848  			errL = append(errL, err)
   849  		}
   850  	} else {
   851  		err := errors.New("request missing required field: Video")
   852  		errL = append(errL, err)
   853  	}
   854  
   855  	return errL, podErrors
   856  }
   857  
   858  func isZeroOrNegativeDuration(duration []int) bool {
   859  	for _, value := range duration {
   860  		if value <= 0 {
   861  			return true
   862  		}
   863  	}
   864  	return false
   865  }