Repository: prebid/prebid-server
Pull request #2196 (2022-03-22 01:04:56)

First party data fix for resolved request
Added original request copy before First party data extraction to save it for resolved request in debug mode.


Patch:
diff --git a/adapters/yieldlab/yieldlab.go b/adapters/yieldlab/yieldlab.go
index 2c9892aa..1447aeaf 100644
--- a/adapters/yieldlab/yieldlab.go
+++ b/adapters/yieldlab/yieldlab.go
@@ -76,8 +76,10 @@ func (a *YieldlabAdapter) makeEndpointURL(req *openrtb2.BidRequest, params *open
 	if err != nil {
 		return "", err
 	}
-	if gdpr != "" && consent != "" {
+	if gdpr != "" {
 		q.Set("gdpr", gdpr)
+	}
+	if consent != "" {
 		q.Set("consent", consent)
 	}
 
@@ -87,17 +89,6 @@ func (a *YieldlabAdapter) makeEndpointURL(req *openrtb2.BidRequest, params *open
 }
 
 func (a *YieldlabAdapter) getGDPR(request *openrtb2.BidRequest) (string, string, error) {
-	gdpr := ""
-	var extRegs openrtb_ext.ExtRegs
-	if request.Regs != nil {
-		if err := json.Unmarshal(request.Regs.Ext, &extRegs); err != nil {
-			return "", "", fmt.Errorf("failed to parse ExtRegs in Yieldlab GDPR check: %v", err)
-		}
-		if extRegs.GDPR != nil && (*extRegs.GDPR == 0 || *extRegs.GDPR == 1) {
-			gdpr = strconv.Itoa(int(*extRegs.GDPR))
-		}
-	}
-
 	consent := ""
 	if request.User != nil && request.User.Ext != nil {
 		var extUser openrtb_ext.ExtUser
@@ -107,6 +98,16 @@ func (a *YieldlabAdapter) getGDPR(request *openrtb2.BidRequest) (string, string,
 		consent = extUser.Consent
 	}
 
+	gdpr := ""
+	var extRegs openrtb_ext.ExtRegs
+	if request.Regs != nil {
+		if err := json.Unmarshal(request.Regs.Ext, &extRegs); err == nil {
+			if extRegs.GDPR != nil && (*extRegs.GDPR == 0 || *extRegs.GDPR == 1) {
+				gdpr = strconv.Itoa(int(*extRegs.GDPR))
+			}
+		}
+	}
+
 	return gdpr, consent, nil
 }
 
diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go
index 6248a1f9..17d083a1 100644
--- a/endpoints/openrtb2/amp_auction.go
+++ b/endpoints/openrtb2/amp_auction.go
@@ -193,7 +193,7 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h
 	secGPC := r.Header.Get("Sec-GPC")
 
 	auctionRequest := exchange.AuctionRequest{
-		BidRequest:                 req,
+		BidRequestWrapper:          &openrtb_ext.RequestWrapper{BidRequest: req},
 		Account:                    *account,
 		UserSyncs:                  usersyncs,
 		RequestType:                labels.RType,
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index 3b6a677b..a62460e8 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -5,6 +5,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"github.com/prebid/prebid-server/gdpr"
 	"io"
 	"io/ioutil"
 	"net/http"
@@ -30,8 +31,6 @@ import (
 	"github.com/prebid/prebid-server/currency"
 	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/exchange"
-	"github.com/prebid/prebid-server/firstpartydata"
-	"github.com/prebid/prebid-server/gdpr"
 	"github.com/prebid/prebid-server/metrics"
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/prebid/prebid-server/prebid_cache_client"
@@ -157,15 +156,6 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 		return
 	}
 
-	resolvedFPD, fpdErrors := firstpartydata.ExtractFPDForBidders(req)
-	if len(fpdErrors) > 0 {
-		if errortypes.ContainsFatalError(fpdErrors) && writeError(fpdErrors, w, &labels) {
-			return
-		}
-		errL = append(errL, fpdErrors...)
-	}
-	warnings := errortypes.WarningOnly(errL)
-
 	ctx := context.Background()
 
 	timeout := deps.cfg.AuctionTimeouts.LimitAuctionTimeout(time.Duration(req.TMax) * time.Millisecond)
@@ -205,18 +195,12 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 		writeError(errL, w, &labels)
 		return
 	}
-
-	// rebuild/resync the request in the request wrapper.
-	if err := req.RebuildRequest(); err != nil {
-		errL = append(errL, err)
-		writeError(errL, w, &labels)
-		return
-	}
-
 	secGPC := r.Header.Get("Sec-GPC")
 
+	warnings := errortypes.WarningOnly(errL)
+
 	auctionRequest := exchange.AuctionRequest{
-		BidRequest:                 req.BidRequest,
+		BidRequestWrapper:          req,
 		Account:                    *account,
 		UserSyncs:                  usersyncs,
 		RequestType:                labels.RType,
@@ -225,7 +209,6 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 		Warnings:                   warnings,
 		GlobalPrivacyControlHeader: secGPC,
 		ImpExtInfoMap:              impExtInfoMap,
-		FirstPartyData:             resolvedFPD,
 		StoredAuctionResponses:     storedAuctionResponses,
 		TCF2ConfigBuilder:          gdpr.NewTCF2Config,
 		GDPRPermissionsBuilder:     gdpr.NewPermissions,
@@ -236,6 +219,10 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 	ao.Response = response
 	ao.Account = account
 	if err != nil {
+		if errortypes.ReadCode(err) == errortypes.BadInputErrorCode {
+			writeError([]error{err}, w, &labels)
+			return
+		}
 		labels.RequestStatus = metrics.RequestStatusErr
 		w.WriteHeader(http.StatusInternalServerError)
 		fmt.Fprintf(w, "Critical error while running the auction: %v", err)
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/first-party-data.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/first-party-data.json
deleted file mode 100644
index 4552b883..00000000
--- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/first-party-data.json
+++ /dev/null
@@ -1,156 +0,0 @@
-{
-  "description": "Valid request with First party data for two bidders, both have global and bidder specific fpd",
-  "mockBidRequest": {
-    "id": "bid_req_id",
-    "imp": [
-      {
-        "id": "1_0",
-        "video": {
-          "mimes": [
-            "video/mp4"
-          ],
-          "maxduration": 30,
-          "protocols": [
-            2,
-            3
-          ],
-          "w": 640,
-          "h": 480
-        },
-        "ext": {
-          "appnexus": {
-            "placementId": 123
-          },
-          "telaria": {
-            "seatCode": "test",
-            "adCode": "test"
-          }
-        }
-      }
-    ],
-    "site": {
-      "id": "reqSiteId",
-      "page": "http://www.foobar.com/1234.html",
-      "publisher": {
-        "id": "1"
-      },
-      "content": {
-        "episode": 6,
-        "title": "episodeName",
-        "series": "TvName",
-        "season": "season3",
-        "len": 900,
-        "data": [
-          {
-            "id": "reqContentDataSiteId1",
-            "name": "reqContentDataSiteName1"
-          }
-        ]
-      },
-      "ext": {
-        "amp": 1,
-        "data": {
-          "somesitefpd": "fpdSite"
-        }
-      }
-    },
-    "user": {
-      "id": "reqUserID",
-      "keywords": "userKeyword",
-      "yob": 1982,
-      "gender": "M",
-      "data": [
-        {
-          "id": "reqDataUserId1",
-          "name": "reqDataUserName1"
-        },
-        {
-          "id": "reqDataUserId2",
-          "name": "reqDataUserName2"
-        }
-      ],
-      "ext": {
-        "data": {
-          "moreuserdata": "morefpduserdata"
-        }
-      }
-    },
-    "test": 1,
-    "at": 1,
-    "tmax": 5000,
-    "ext": {
-      "prebid": {
-        "data": {
-          "bidders": [
-            "bidder1",
-            "bidder2"
-          ]
-        },
-        "bidderconfig": [
-          {
-            "bidders": [
-              "bidder1"
-            ],
-            "config": {
-              "ortb2": {
-                "site": {
-                  "id": "fpdSiteId",
-                  "keywords": "fpd keywords!",
-                  "data": [
-                    {
-                      "id": "siteId1",
-                      "name": true
-                    },
-                    {
-                      "id": "siteId2",
-                      "name": false
-                    }
-                  ],
-                  "page": "",
-                  "ext": {
-                    "data": {
-                      "morefpdData": "morefpddata",
-                      "sitefpddata": "mysitefpddata"
-                    }
-                  }
-                }
-              }
-            }
-          },
-          {
-            "bidders": [
-              "bidder2"
-            ],
-            "config": {
-              "ortb2": {
-                "user": {
-                  "id": "fpdUserId",
-                  "yob": 2011,
-                  "gender": "F",
-                  "keywords": "fpd keywords",
-                  "data": [
-                    {
-                      "id": "fpdUserDataId1",
-                      "name": "fpdUserDataName1"
-                    },
-                    {
-                      "id": "fpdUserDataId2",
-                      "name": "fpdUserDataName2"
-                    }
-                  ],
-                  "ext": {
-                    "data": {
-                      "userdata": "biddermyfpduserdata"
-                    }
-                  }
-                }
-              }
-            }
-          }
-        ]
-      }
-    }
-  },
-  "expectedBidResponse": {},
-  "expectedReturnCode": 200
-}
diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go
index d3324a34..39446cfe 100644
--- a/endpoints/openrtb2/video_auction.go
+++ b/endpoints/openrtb2/video_auction.go
@@ -287,7 +287,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 	secGPC := r.Header.Get("Sec-GPC")
 
 	auctionRequest := exchange.AuctionRequest{
-		BidRequest:                 bidReq,
+		BidRequestWrapper:          &openrtb_ext.RequestWrapper{BidRequest: bidReq},
 		Account:                    *account,
 		UserSyncs:                  usersyncs,
 		RequestType:                labels.RType,
diff --git a/exchange/exchange.go b/exchange/exchange.go
index 1d41f891..af28f2bb 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -151,7 +151,8 @@ type ImpExtInfo struct {
 // AuctionRequest holds the bid request for the auction
 // and all other information needed to process that request
 type AuctionRequest struct {
-	BidRequest                 *openrtb2.BidRequest
+	BidRequestWrapper          *openrtb_ext.RequestWrapper
+	ResolvedBidRequest         json.RawMessage
 	Account                    config.Account
 	UserSyncs                  IdFetcher
 	RequestType                metrics.RequestType
@@ -180,8 +181,12 @@ type BidderRequest struct {
 }
 
 func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *DebugLog) (*openrtb2.BidResponse, error) {
-	var err error
-	requestExt, err := extractBidRequestExt(r.BidRequest)
+	var errs []error
+	// rebuild/resync the request in the request wrapper.
+	if err := r.BidRequestWrapper.RebuildRequest(); err != nil {
+		return nil, err
+	}
+	requestExt, err := extractBidRequestExt(r.BidRequestWrapper.BidRequest)
 	if err != nil {
 		return nil, err
 	}
@@ -191,15 +196,48 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *
 	if targData != nil {
 		_, targData.cacheHost, targData.cachePath = e.cache.GetExtCacheData()
 	}
+	responseDebugAllow, accountDebugAllow, debugLog := getDebugInfo(r.BidRequestWrapper.BidRequest, requestExt, r.Account.DebugAllow, debugLog)
+	if responseDebugAllow {
+		//save incoming request with stored requests (if applicable) to return in debug logs
+		resolvedBidReq, err := json.Marshal(r.BidRequestWrapper.BidRequest)
+		if err != nil {
+			return nil, err
+		}
+		r.ResolvedBidRequest = resolvedBidReq
+	}
 
-	responseDebugAllow, accountDebugAllow, debugLog := getDebugInfo(r.BidRequest, requestExt, r.Account.DebugAllow, debugLog)
+	if r.RequestType == metrics.ReqTypeORTB2Web || r.RequestType == metrics.ReqTypeORTB2App {
+		//Extract First party data for auction endpoint only
+		resolvedFPD, fpdErrors := firstpartydata.ExtractFPDForBidders(r.BidRequestWrapper)
+		if len(fpdErrors) > 0 {
+			var errMessages []string
+			for _, fpdError := range fpdErrors {
+				errMessages = append(errMessages, fpdError.Error())
+			}
+			return nil, &errortypes.BadInput{
+				Message: strings.Join(errMessages, ","),
+			}
+		}
+		r.FirstPartyData = resolvedFPD
+		if len(resolvedFPD) > 0 {
+			// rebuild/resync the request in the request wrapper.
+			// it needs to be here to update ReqWrapper and requestExt because req.ext was modified after FPD extraction
+			if err := r.BidRequestWrapper.RebuildRequest(); err != nil {
+				return nil, err
+			}
+			requestExt, err = extractBidRequestExt(r.BidRequestWrapper.BidRequest)
+			if err != nil {
+				return nil, err
+			}
+		}
+	}
 
 	bidAdjustmentFactors := getExtBidAdjustmentFactors(requestExt)
 
-	recordImpMetrics(r.BidRequest, e.me)
+	recordImpMetrics(r.BidRequestWrapper.BidRequest, e.me)
 
 	// Make our best guess if GDPR applies
-	gdprDefaultValue := e.parseGDPRDefaultValue(r.BidRequest)
+	gdprDefaultValue := e.parseGDPRDefaultValue(r.BidRequestWrapper.BidRequest)
 
 	// Slice of BidRequests, each a copy of the original cleaned to only contain bidder data for the named bidder
 	tcf2Cfg := r.TCF2ConfigBuilder(e.privacyConfig.GDPR.TCF2, r.Account.GDPR)
@@ -270,11 +308,11 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *
 
 		if targData != nil {
 			// A non-nil auction is only needed if targeting is active. (It is used below this block to extract cache keys)
-			auc = newAuction(adapterBids, len(r.BidRequest.Imp), targData.preferDeals)
+			auc = newAuction(adapterBids, len(r.BidRequestWrapper.BidRequest.Imp), targData.preferDeals)
 			auc.setRoundedPrices(targData.priceGranularity)
 
 			if requestExt.Prebid.SupportDeals {
-				dealErrs := applyDealSupport(r.BidRequest, auc, bidCategory)
+				dealErrs := applyDealSupport(r.BidRequestWrapper.BidRequest, auc, bidCategory)
 				errs = append(errs, dealErrs...)
 			}
 
@@ -288,12 +326,12 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *
 				}
 			}
 
-			cacheErrs = auc.doCache(ctx, e.cache, targData, evTracking, r.BidRequest, 60, &r.Account.CacheTTL, bidCategory, debugLog)
+			cacheErrs = auc.doCache(ctx, e.cache, targData, evTracking, r.BidRequestWrapper.BidRequest, 60, &r.Account.CacheTTL, bidCategory, debugLog)
 			if len(cacheErrs) > 0 {
 				errs = append(errs, cacheErrs...)
 			}
 
-			targData.setTargeting(auc, r.BidRequest.App != nil, bidCategory, r.Account.TruncateTargetAttribute)
+			targData.setTargeting(auc, r.BidRequestWrapper.BidRequest.App != nil, bidCategory, r.Account.TruncateTargetAttribute)
 
 		}
 		bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, r, responseDebugAllow, errs)
@@ -328,7 +366,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *
 	}
 
 	// Build the response
-	return e.buildBidResponse(ctx, liveAdapters, adapterBids, r.BidRequest, adapterExtra, auc, bidResponseExt, cacheInstructions.returnCreative, r.ImpExtInfoMap, errs)
+	return e.buildBidResponse(ctx, liveAdapters, adapterBids, r.BidRequestWrapper.BidRequest, adapterExtra, auc, bidResponseExt, cacheInstructions.returnCreative, r.ImpExtInfoMap, errs)
 }
 
 func (e *exchange) parseGDPRDefaultValue(bidRequest *openrtb2.BidRequest) gdpr.Signal {
@@ -876,17 +914,17 @@ func getPrimaryAdServer(adServerId int) (string, error) {
 
 // Extract all the data from the SeatBids and build the ExtBidResponse
 func (e *exchange) makeExtBidResponse(adapterBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, r AuctionRequest, debugInfo bool, errList []error) *openrtb_ext.ExtBidResponse {
-	req := r.BidRequest
+
 	bidResponseExt := &openrtb_ext.ExtBidResponse{
 		Errors:               make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage, len(adapterBids)),
 		Warnings:             make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage, len(adapterBids)),
 		ResponseTimeMillis:   make(map[openrtb_ext.BidderName]int, len(adapterBids)),
-		RequestTimeoutMillis: req.TMax,
+		RequestTimeoutMillis: r.BidRequestWrapper.BidRequest.TMax,
 	}
 	if debugInfo {
 		bidResponseExt.Debug = &openrtb_ext.ExtResponseDebug{
 			HttpCalls:       make(map[openrtb_ext.BidderName][]*openrtb_ext.ExtHttpCall),
-			ResolvedRequest: req,
+			ResolvedRequest: r.ResolvedBidRequest,
 		}
 	}
 	if !r.StartTime.IsZero() {
diff --git a/exchange/utils.go b/exchange/utils.go
index 2204e6ac..429d29b9 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -6,10 +6,9 @@ import (
 	"fmt"
 	"math/rand"
 
+	"github.com/buger/jsonparser"
 	"github.com/mxmCherry/openrtb/v15/openrtb2"
 	"github.com/prebid/go-gdpr/vendorconsent"
-
-	"github.com/buger/jsonparser"
 	"github.com/prebid/prebid-server/adapters"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/firstpartydata"
@@ -37,7 +36,7 @@ const unknownBidder string = ""
 //   2. Every BidRequest.Imp[] requested Bids from the Bidder who keys it.
 //   3. BidRequest.User.BuyerUID will be set to that Bidder's ID.
 func cleanOpenRTBRequests(ctx context.Context,
-	req AuctionRequest,
+	auctionReq AuctionRequest,
 	requestExt *openrtb_ext.ExtRequest,
 	bidderToSyncerKey map[string]string,
 	metricsEngine metrics.MetricsEngine,
@@ -47,6 +46,7 @@ func cleanOpenRTBRequests(ctx context.Context,
 	tcf2Cfg gdpr.TCF2ConfigReader,
 ) (allowedBidderRequests []BidderRequest, privacyLabels metrics.PrivacyLabels, errs []error) {
 
+	req := auctionReq.BidRequestWrapper
 	impsByBidder, err := splitImps(req.BidRequest.Imp)
 	if err != nil {
 		errs = []error{err}
@@ -64,7 +64,7 @@ func cleanOpenRTBRequests(ctx context.Context,
 	}
 
 	var allBidderRequests []BidderRequest
-	allBidderRequests, errs = getAuctionBidderRequests(req, requestExt, bidderToSyncerKey, impsByBidder, aliases)
+	allBidderRequests, errs = getAuctionBidderRequests(auctionReq, requestExt, bidderToSyncerKey, impsByBidder, aliases)
 
 	if len(allBidderRequests) == 0 {
 		return
@@ -80,7 +80,7 @@ func cleanOpenRTBRequests(ctx context.Context,
 	}
 	gdprApplies := gdprSignal == gdpr.SignalYes || (gdprSignal == gdpr.SignalAmbiguous && gdprDefaultValue == gdpr.SignalYes)
 
-	ccpaEnforcer, err := extractCCPA(req.BidRequest, privacyConfig, &req.Account, aliases, integrationTypeMap[req.LegacyLabels.RType])
+	ccpaEnforcer, err := extractCCPA(req.BidRequest, privacyConfig, &auctionReq.Account, aliases, integrationTypeMap[auctionReq.LegacyLabels.RType])
 	if err != nil {
 		errs = append(errs, err)
 	}
@@ -100,7 +100,7 @@ func cleanOpenRTBRequests(ctx context.Context,
 
 	var gdprEnforced bool
 	if gdprApplies {
-		gdprEnforced = tcf2Cfg.IntegrationEnabled(integrationTypeMap[req.LegacyLabels.RType])
+		gdprEnforced = tcf2Cfg.IntegrationEnabled(integrationTypeMap[auctionReq.LegacyLabels.RType])
 	}
 
 	if gdprEnforced {
@@ -122,7 +122,7 @@ func cleanOpenRTBRequests(ctx context.Context,
 
 		// GDPR
 		if gdprEnforced {
-			var publisherID = req.LegacyLabels.PubID
+			var publisherID = auctionReq.LegacyLabels.PubID
 			bidReq, geo, id, err := gdprPerms.AuctionActivitiesAllowed(ctx, bidderRequest.BidderCoreName, bidderRequest.BidderName, publisherID, gdprSignal, consent, aliasesGVLIDs)
 			bidRequestAllowed = bidReq
 
@@ -139,8 +139,8 @@ func cleanOpenRTBRequests(ctx context.Context,
 			}
 		}
 
-		if req.FirstPartyData != nil && req.FirstPartyData[bidderRequest.BidderName] != nil {
-			applyFPD(req.FirstPartyData[bidderRequest.BidderName], bidderRequest.BidRequest)
+		if auctionReq.FirstPartyData != nil && auctionReq.FirstPartyData[bidderRequest.BidderName] != nil {
+			applyFPD(auctionReq.FirstPartyData[bidderRequest.BidderName], bidderRequest.BidRequest)
 		}
 
 		if bidRequestAllowed {
@@ -186,14 +186,14 @@ func extractLMT(orig *openrtb2.BidRequest, privacyConfig config.Privacy) privacy
 	}
 }
 
-func getAuctionBidderRequests(req AuctionRequest,
+func getAuctionBidderRequests(auctionRequest AuctionRequest,
 	requestExt *openrtb_ext.ExtRequest,
 	bidderToSyncerKey map[string]string,
 	impsByBidder map[string][]openrtb2.Imp,
 	aliases map[string]string) ([]BidderRequest, []error) {
 
 	bidderRequests := make([]BidderRequest, 0, len(impsByBidder))
-
+	req := auctionRequest.BidRequestWrapper
 	explicitBuyerUIDs, err := extractBuyerUIDs(req.BidRequest.User)
 	if err != nil {
 		return nil, []error{err}
@@ -246,17 +246,17 @@ func getAuctionBidderRequests(req AuctionRequest,
 			BidderCoreName: coreBidder,
 			BidRequest:     &reqCopy,
 			BidderLabels: metrics.AdapterLabels{
-				Source:      req.LegacyLabels.Source,
-				RType:       req.LegacyLabels.RType,
+				Source:      auctionRequest.LegacyLabels.Source,
+				RType:       auctionRequest.LegacyLabels.RType,
 				Adapter:     coreBidder,
-				PubID:       req.LegacyLabels.PubID,
-				CookieFlag:  req.LegacyLabels.CookieFlag,
+				PubID:       auctionRequest.LegacyLabels.PubID,
+				CookieFlag:  auctionRequest.LegacyLabels.CookieFlag,
 				AdapterBids: metrics.AdapterBidPresent,
 			},
 		}
 
 		syncerKey := bidderToSyncerKey[string(coreBidder)]
-		if hadSync := prepareUser(&reqCopy, bidder, syncerKey, explicitBuyerUIDs, req.UserSyncs); !hadSync && req.BidRequest.App == nil {
+		if hadSync := prepareUser(&reqCopy, bidder, syncerKey, explicitBuyerUIDs, auctionRequest.UserSyncs); !hadSync && req.BidRequest.App == nil {
 			bidderRequest.BidderLabels.CookieFlag = metrics.CookieFlagNo
 		} else {
 			bidderRequest.BidderLabels.CookieFlag = metrics.CookieFlagYes
diff --git a/openrtb_ext/response.go b/openrtb_ext/response.go
index 1c7177da..044bc0bd 100644
--- a/openrtb_ext/response.go
+++ b/openrtb_ext/response.go
@@ -1,6 +1,8 @@
 package openrtb_ext
 
-import "github.com/mxmCherry/openrtb/v15/openrtb2"
+import (
+	"encoding/json"
+)
 
 // ExtBidResponse defines the contract for bidresponse.ext
 type ExtBidResponse struct {
@@ -25,7 +27,7 @@ type ExtResponseDebug struct {
 	// HttpCalls defines the contract for bidresponse.ext.debug.httpcalls
 	HttpCalls map[BidderName][]*ExtHttpCall `json:"httpcalls,omitempty"`
 	// Request after resolution of stored requests and debug overrides
-	ResolvedRequest *openrtb2.BidRequest `json:"resolvedrequest,omitempty"`
+	ResolvedRequest json.RawMessage `json:"resolvedrequest,omitempty"`
 }
 
 // ExtResponseSyncData defines the contract for bidresponse.ext.usersync.{bidder}


Test patch:
diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go
index 1e32727b..1999f2da 100644
--- a/endpoints/openrtb2/amp_auction_test.go
+++ b/endpoints/openrtb2/amp_auction_test.go
@@ -790,7 +790,9 @@ func TestQueryParamOverrides(t *testing.T) {
 		t.Fatalf("Error unmarshalling response: %s", err.Error())
 	}
 
-	resolvedRequest := response.Debug.ResolvedRequest
+	var resolvedRequest openrtb2.BidRequest
+	err := json.Unmarshal(response.Debug.ResolvedRequest, &resolvedRequest)
+	assert.NoError(t, err, "resolved request should have a correct format")
 	if resolvedRequest.TMax != timeout {
 		t.Errorf("Expected TMax to equal timeout (%d), got: %d", timeout, resolvedRequest.TMax)
 	}
@@ -936,8 +938,10 @@ func (s formatOverrideSpec) execute(t *testing.T) {
 	if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
 		t.Fatalf("Error unmarshalling response: %s", err.Error())
 	}
-
-	formats := response.Debug.ResolvedRequest.Imp[0].Banner.Format
+	var resolvedRequest openrtb2.BidRequest
+	err := json.Unmarshal(response.Debug.ResolvedRequest, &resolvedRequest)
+	assert.NoError(t, err, "resolved request should have the correct format")
+	formats := resolvedRequest.Imp[0].Banner.Format
 	if len(formats) != len(s.expect) {
 		t.Fatalf("Bad formats length. Expected %v, got %v", s.expect, formats)
 	}
@@ -988,7 +992,8 @@ var expectedErrorsFromHoldAuction map[openrtb_ext.BidderName][]openrtb_ext.ExtBi
 	},
 }
 
-func (m *mockAmpExchange) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
+func (m *mockAmpExchange) HoldAuction(ctx context.Context, auctionRequest exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
+	r := auctionRequest.BidRequestWrapper
 	m.lastRequest = r.BidRequest
 
 	response := &openrtb2.BidResponse{
@@ -1000,10 +1005,10 @@ func (m *mockAmpExchange) HoldAuction(ctx context.Context, r exchange.AuctionReq
 		}},
 		Ext: json.RawMessage(`{ "errors": {"openx":[ { "code": 1, "message": "The request exceeded the timeout allocated" } ] } }`),
 	}
-	if len(r.StoredAuctionResponses) > 0 {
+	if len(auctionRequest.StoredAuctionResponses) > 0 {
 		var seatBids []openrtb2.SeatBid
 
-		if err := json.Unmarshal(r.StoredAuctionResponses[r.BidRequest.Imp[0].ID], &seatBids); err != nil {
+		if err := json.Unmarshal(auctionRequest.StoredAuctionResponses[r.BidRequest.Imp[0].ID], &seatBids); err != nil {
 			return nil, err
 		}
 		response.SeatBid = seatBids
diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go
index 0f56036d..0bb79798 100644
--- a/endpoints/openrtb2/auction_test.go
+++ b/endpoints/openrtb2/auction_test.go
@@ -17,8 +17,6 @@ import (
 	"testing"
 	"time"
 
-	"github.com/prebid/prebid-server/firstpartydata"
-
 	analyticsConf "github.com/prebid/prebid-server/analytics/config"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/currency"
@@ -3225,57 +3223,6 @@ func TestParseRequestParseImpInfoError(t *testing.T) {
 	assert.Contains(t, errL[0].Error(), "echovideoattrs of type bool", "Incorrect error message")
 }
 
-func TestAuctionFirstPartyData(t *testing.T) {
-	reqBody := validRequest(t, "first-party-data.json")
-	deps := &endpointDeps{
-		fakeUUIDGenerator{},
-		&mockExchangeFPD{},
-		newParamsValidator(t),
-		&mockStoredReqFetcher{},
-		empty_fetcher.EmptyFetcher{},
-		empty_fetcher.EmptyFetcher{},
-		&config.Configuration{MaxRequestSize: int64(len(reqBody))},
-		&metricsConfig.NilMetricsEngine{},
-		analyticsConf.NewPBSAnalytics(&config.Analytics{}),
-		map[string]string{},
-		false,
-		[]byte{},
-		openrtb_ext.BuildBidderMap(),
-		nil,
-		nil,
-		hardcodedResponseIPValidator{response: true},
-		empty_fetcher.EmptyFetcher{},
-	}
-
-	req := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(reqBody))
-	recorder := httptest.NewRecorder()
-
-	deps.Auction(recorder, req, nil)
-
-	if recorder.Code != http.StatusOK {
-		t.Errorf("Endpoint should return a 200")
-	}
-	resultRequest := deps.ex.(*mockExchangeFPD).lastRequest
-	resultFPD := deps.ex.(*mockExchangeFPD).firstPartyData
-
-	assert.Len(t, resultFPD, 2, "Result FPD length is incorrect")
-
-	assert.NotNil(t, resultFPD[openrtb_ext.BidderName("bidder1")], "Result FPD for bidder1 is incorrect")
-	assert.NotNil(t, resultFPD[openrtb_ext.BidderName("bidder1")].Site, "Result FPD for bidder1.Site is incorrect")
-	assert.Nil(t, resultFPD[openrtb_ext.BidderName("bidder1")].App, "Result FPD for bidder1.App is incorrect")
-	assert.NotNil(t, resultFPD[openrtb_ext.BidderName("bidder1")].User, "Result FPD for bidder1.User is incorrect")
-
-	assert.NotNil(t, resultFPD[openrtb_ext.BidderName("bidder2")], "Result FPD for bidder2 is incorrect")
-	assert.NotNil(t, resultFPD[openrtb_ext.BidderName("bidder2")].Site, "Result FPD for bidder2.Site is incorrect")
-	assert.Nil(t, resultFPD[openrtb_ext.BidderName("bidder2")].App, "Result FPD for bidder2.App is incorrect")
-	assert.NotNil(t, resultFPD[openrtb_ext.BidderName("bidder2")].User, "Result FPD for bidder2.User is incorrect")
-
-	assert.Nil(t, resultRequest.App, "Result request App should be nil")
-	assert.Nil(t, resultRequest.Site.Content.Data, "Result request Site.Content.Data is incorrect")
-	assert.JSONEq(t, string(resultRequest.Site.Ext), `{"amp": 1}`, "Result request Site.Ext is incorrect")
-	assert.Nil(t, resultRequest.User.Ext, "Result request User.Ext is incorrect")
-}
-
 func TestValidateNativeContextTypes(t *testing.T) {
 	impIndex := 4
 
@@ -3765,7 +3712,8 @@ type nobidExchange struct {
 	gotRequest *openrtb2.BidRequest
 }
 
-func (e *nobidExchange) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
+func (e *nobidExchange) HoldAuction(ctx context.Context, auctionRequest exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
+	r := auctionRequest.BidRequestWrapper
 	e.gotRequest = r.BidRequest
 	return &openrtb2.BidResponse{
 		ID:    r.BidRequest.ID,
@@ -3822,7 +3770,8 @@ func (e *mockBidExchange) getAuctionCurrencyRates(customRates *openrtb_ext.ExtRe
 
 // mockBidExchange is a well-behaved exchange that lists the bidders found in every bidRequest.Imp[i].Ext
 // into the bidResponse.Ext to assert the bidder adapters that were not filtered out in the validation process
-func (e *mockBidExchange) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
+func (e *mockBidExchange) HoldAuction(ctx context.Context, auctionRequest exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
+	r := auctionRequest.BidRequestWrapper
 	bidResponse := &openrtb2.BidResponse{
 		ID:    r.BidRequest.ID,
 		BidID: "test bid id",
@@ -4586,7 +4535,7 @@ type mockExchange struct {
 }
 
 func (m *mockExchange) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
-	m.lastRequest = r.BidRequest
+	m.lastRequest = r.BidRequestWrapper.BidRequest
 	return &openrtb2.BidResponse{
 		SeatBid: []openrtb2.SeatBid{{
 			Bid: []openrtb2.Bid{{
@@ -4596,17 +4545,6 @@ func (m *mockExchange) HoldAuction(ctx context.Context, r exchange.AuctionReques
 	}, nil
 }
 
-type mockExchangeFPD struct {
-	lastRequest    *openrtb2.BidRequest
-	firstPartyData map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData
-}
-
-func (m *mockExchangeFPD) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
-	m.lastRequest = r.BidRequest
-	m.firstPartyData = r.FirstPartyData
-	return &openrtb2.BidResponse{}, nil
-}
-
 func getBidderInfos(cfg map[string]config.Adapter, biddersNames []openrtb_ext.BidderName) config.BidderInfos {
 	biddersInfos := make(config.BidderInfos)
 	for _, name := range biddersNames {
diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go
index c4d8602a..0969bca7 100644
--- a/endpoints/openrtb2/video_auction_test.go
+++ b/endpoints/openrtb2/video_auction_test.go
@@ -1322,7 +1322,7 @@ type mockExchangeVideo struct {
 }
 
 func (m *mockExchangeVideo) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
-	m.lastRequest = r.BidRequest
+	m.lastRequest = r.BidRequestWrapper.BidRequest
 	if debugLog != nil && debugLog.Enabled {
 		m.cache.called = true
 	}
@@ -1358,7 +1358,7 @@ type mockExchangeAppendBidderNames struct {
 }
 
 func (m *mockExchangeAppendBidderNames) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
-	m.lastRequest = r.BidRequest
+	m.lastRequest = r.BidRequestWrapper.BidRequest
 	if debugLog != nil && debugLog.Enabled {
 		m.cache.called = true
 	}
@@ -1394,7 +1394,7 @@ type mockExchangeVideoNoBids struct {
 }
 
 func (m *mockExchangeVideoNoBids) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
-	m.lastRequest = r.BidRequest
+	m.lastRequest = r.BidRequestWrapper.BidRequest
 	return &openrtb2.BidResponse{
 		SeatBid: []openrtb2.SeatBid{{}},
 	}, nil
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index 71393abd..d304f76d 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -18,7 +18,6 @@ import (
 	"github.com/buger/jsonparser"
 	"github.com/mxmCherry/openrtb/v15/openrtb2"
 	"github.com/prebid/go-gdpr/vendorlist"
-	"github.com/prebid/prebid-server/firstpartydata"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
 	jsonpatch "gopkg.in/evanphx/json-patch.v4"
@@ -334,7 +333,7 @@ func TestDebugBehaviour(t *testing.T) {
 		}
 
 		auctionRequest := AuctionRequest{
-			BidRequest:             bidRequest,
+			BidRequestWrapper:      &openrtb_ext.RequestWrapper{BidRequest: bidRequest},
 			Account:                config.Account{DebugAllow: test.debugData.accountLevelDebugAllowed},
 			UserSyncs:              &emptyUsersync{},
 			StartTime:              time.Now(),
@@ -382,7 +381,9 @@ func TestDebugBehaviour(t *testing.T) {
 
 			// If not nil, assert bid extension
 			if test.in.debug {
-				assert.JSONEq(t, string(bidRequest.Ext), string(actualExt.Debug.ResolvedRequest.Ext), test.desc)
+				actualResolvedReqExt, _, _, err := jsonparser.Get(actualExt.Debug.ResolvedRequest, "ext")
+				assert.NoError(t, err, "Resolved request should have the correct format")
+				assert.JSONEq(t, string(bidRequest.Ext), string(actualResolvedReqExt), test.desc)
 			}
 		} else if !test.debugData.bidderLevelDebugAllowed && test.debugData.accountLevelDebugAllowed {
 			assert.Equal(t, len(actualExt.Debug.HttpCalls), 0, "%s. ext.debug.httpcalls array should not be empty", "With bidder level debug disable option http calls should be empty")
@@ -492,7 +493,7 @@ func TestTwoBiddersDebugDisabledAndEnabled(t *testing.T) {
 		bidRequest.Ext = json.RawMessage(`{"prebid":{"debug":true}}`)
 
 		auctionRequest := AuctionRequest{
-			BidRequest:             bidRequest,
+			BidRequestWrapper:      &openrtb_ext.RequestWrapper{BidRequest: bidRequest},
 			Account:                config.Account{DebugAllow: true},
 			UserSyncs:              &emptyUsersync{},
 			StartTime:              time.Now(),
@@ -666,7 +667,7 @@ func TestOverrideWithCustomCurrency(t *testing.T) {
 		mockBidRequest.Cur = []string{test.in.bidRequestCurrency}
 
 		auctionRequest := AuctionRequest{
-			BidRequest:             mockBidRequest,
+			BidRequestWrapper:      &openrtb_ext.RequestWrapper{BidRequest: mockBidRequest},
 			Account:                config.Account{},
 			UserSyncs:              &emptyUsersync{},
 			GDPRPermissionsBuilder: mockGDPRPermissionsBuilder,
@@ -752,7 +753,7 @@ func TestAdapterCurrency(t *testing.T) {
 
 	// Run Auction
 	auctionRequest := AuctionRequest{
-		BidRequest:             request,
+		BidRequestWrapper:      &openrtb_ext.RequestWrapper{BidRequest: request},
 		Account:                config.Account{},
 		UserSyncs:              &emptyUsersync{},
 		GDPRPermissionsBuilder: mockGDPRPermissionsBuilder,
@@ -1139,7 +1140,7 @@ func TestReturnCreativeEndToEnd(t *testing.T) {
 			mockBidRequest.Ext = test.inExt
 
 			auctionRequest := AuctionRequest{
-				BidRequest:             mockBidRequest,
+				BidRequestWrapper:      &openrtb_ext.RequestWrapper{BidRequest: mockBidRequest},
 				Account:                config.Account{},
 				UserSyncs:              &emptyUsersync{},
 				GDPRPermissionsBuilder: mockGDPRPermissionsBuilder,
@@ -1806,7 +1807,7 @@ func TestRaceIntegration(t *testing.T) {
 	currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0))
 
 	auctionRequest := AuctionRequest{
-		BidRequest:             getTestBuildRequest(t),
+		BidRequestWrapper:      &openrtb_ext.RequestWrapper{BidRequest: getTestBuildRequest(t)},
 		Account:                config.Account{},
 		UserSyncs:              &emptyUsersync{},
 		GDPRPermissionsBuilder: mockGDPRPermissionsBuilder,
@@ -2016,7 +2017,7 @@ func TestPanicRecoveryHighLevel(t *testing.T) {
 	}
 
 	auctionRequest := AuctionRequest{
-		BidRequest:             request,
+		BidRequestWrapper:      &openrtb_ext.RequestWrapper{BidRequest: request},
 		Account:                config.Account{},
 		UserSyncs:              &emptyUsersync{},
 		GDPRPermissionsBuilder: mockGDPRPermissionsBuilder,
@@ -2126,7 +2127,7 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) {
 	}
 
 	auctionRequest := AuctionRequest{
-		BidRequest: &spec.IncomingRequest.OrtbRequest,
+		BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: &spec.IncomingRequest.OrtbRequest},
 		Account: config.Account{
 			ID:            "testaccount",
 			EventsEnabled: spec.EventsEnabled,
@@ -2139,9 +2140,18 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) {
 	if spec.StartTime > 0 {
 		auctionRequest.StartTime = time.Unix(0, spec.StartTime*1e+6)
 	}
+	if spec.RequestType != nil {
+		auctionRequest.RequestType = *spec.RequestType
+	}
 	ctx := context.Background()
 
 	bid, err := ex.HoldAuction(ctx, auctionRequest, debugLog)
+	if len(spec.Response.Error) > 0 && spec.Response.Bids == nil {
+		if err.Error() != spec.Response.Error {
+			t.Errorf("%s: Exchange returned different errors. Expected %s, got %s", filename, spec.Response.Error, err.Error())
+		}
+		return
+	}
 	responseTimes := extractResponseTimes(t, filename, bid)
 	for _, bidderName := range biddersInAuction {
 		if _, ok := responseTimes[bidderName]; !ok {
@@ -3645,62 +3655,6 @@ func TestMakeBidExtJSON(t *testing.T) {
 	}
 }
 
-func TestFPD(t *testing.T) {
-
-	bidRequest := &openrtb2.BidRequest{
-		ID: "some-request-id",
-		Imp: []openrtb2.Imp{{
-			ID:    "some-impression-id",
-			Video: &openrtb2.Video{W: 100, H: 50},
-			Ext:   json.RawMessage(`{"appnexus": {"placementId": 1}}`),
-		}},
-		Site: &openrtb2.Site{ID: "Req site id", Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)},
-		AT:   1,
-		TMax: 500,
-	}
-
-	fpd := make(map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData)
-
-	apnFpd := firstpartydata.ResolvedFirstPartyData{
-		Site: &openrtb2.Site{ID: "fpdSite"},
-		App:  &openrtb2.App{ID: "fpdApp"},
-		User: &openrtb2.User{ID: "fpdUser"},
-	}
-	fpd[openrtb_ext.BidderName("appnexus")] = &apnFpd
-
-	e := new(exchange)
-	e.adapterMap = map[openrtb_ext.BidderName]adaptedBidder{
-		openrtb_ext.BidderAppnexus: &capturingRequestBidder{},
-	}
-	e.me = &metricsConf.NilMetricsEngine{}
-	e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0))
-
-	ctx := context.Background()
-
-	auctionRequest := AuctionRequest{
-		BidRequest:             bidRequest,
-		UserSyncs:              &emptyUsersync{},
-		FirstPartyData:         fpd,
-		GDPRPermissionsBuilder: mockGDPRPermissionsBuilder,
-		TCF2ConfigBuilder:      mockTCF2ConfigBuilder,
-	}
-
-	debugLog := &DebugLog{DebugOverride: true, DebugEnabledOrOverridden: true}
-
-	outBidResponse, err := e.HoldAuction(ctx, auctionRequest, debugLog)
-
-	assert.NotNilf(t, outBidResponse, "outBidResponse should not be nil")
-	assert.Nil(t, err, "Error should be nil")
-
-	request := e.adapterMap[openrtb_ext.BidderAppnexus].(*capturingRequestBidder).req
-
-	assert.NotNil(t, request, "Bidder request should not be nil")
-	assert.Equal(t, apnFpd.Site, request.Site, "Site is incorrect")
-	assert.Equal(t, apnFpd.App, request.App, "App is incorrect")
-	assert.Equal(t, apnFpd.User, request.User, "User is incorrect")
-
-}
-
 func TestStoredAuctionResponses(t *testing.T) {
 	categoriesFetcher, error := newCategoryFetcher("./test/category-mapping")
 	if error != nil {
@@ -3760,7 +3714,7 @@ func TestStoredAuctionResponses(t *testing.T) {
 	for _, test := range testCases {
 
 		auctionRequest := AuctionRequest{
-			BidRequest:             mockBidRequest,
+			BidRequestWrapper:      &openrtb_ext.RequestWrapper{BidRequest: mockBidRequest},
 			Account:                config.Account{},
 			UserSyncs:              &emptyUsersync{},
 			StoredAuctionResponses: test.storedAuctionResp,
@@ -3933,6 +3887,47 @@ func TestBuildStoredAuctionResponses(t *testing.T) {
 	}
 }
 
+func TestAuctionDebugEnabled(t *testing.T) {
+	categoriesFetcher, err := newCategoryFetcher("./test/category-mapping")
+	assert.NoError(t, err, "error should be nil")
+	e := new(exchange)
+	e.cache = &wellBehavedCache{}
+	e.me = &metricsConf.NilMetricsEngine{}
+	e.vendorListFetcher = gdpr.NewVendorListFetcher(context.Background(), config.GDPR{}, &http.Client{}, gdpr.VendorListURLMaker)
+	e.categoriesFetcher = categoriesFetcher
+	e.bidIDGenerator = &mockBidIDGenerator{false, false}
+	e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0))
+
+	ctx := context.Background()
+
+	bidRequest := &openrtb2.BidRequest{
+		ID:   "some-request-id",
+		Test: 1,
+	}
+
+	auctionRequest := AuctionRequest{
+		BidRequestWrapper:      &openrtb_ext.RequestWrapper{BidRequest: bidRequest},
+		Account:                config.Account{DebugAllow: false},
+		UserSyncs:              &emptyUsersync{},
+		StartTime:              time.Now(),
+		RequestType:            metrics.ReqTypeORTB2Web,
+		GDPRPermissionsBuilder: mockGDPRPermissionsBuilder,
+		TCF2ConfigBuilder:      mockTCF2ConfigBuilder,
+	}
+
+	debugLog := &DebugLog{DebugOverride: true, DebugEnabledOrOverridden: true}
+	resp, err := e.HoldAuction(ctx, auctionRequest, debugLog)
+
+	assert.NoError(t, err, "error should be nil")
+
+	expectedResolvedRequest := `{"id":"some-request-id","imp":null,"test":1}`
+	actualResolvedRequest, _, _, err := jsonparser.Get(resp.Ext, "debug", "resolvedrequest")
+	assert.NoError(t, err, "error should be nil")
+	assert.NotNil(t, actualResolvedRequest, "actualResolvedRequest should not be nil")
+	assert.JSONEq(t, expectedResolvedRequest, string(actualResolvedRequest), "Resolved request is incorrect")
+
+}
+
 type exchangeSpec struct {
 	GDPREnabled       bool                   `json:"gdpr_enabled"`
 	IncomingRequest   exchangeRequest        `json:"incomingRequest"`
@@ -3945,6 +3940,7 @@ type exchangeSpec struct {
 	EventsEnabled     bool                   `json:"events_enabled,omitempty"`
 	StartTime         int64                  `json:"start_time_ms,omitempty"`
 	BidIDGenerator    *mockBidIDGenerator    `json:"bidIDGenerator,omitempty"`
+	RequestType       *metrics.RequestType   `json:"requestType,omitempty"`
 }
 
 type exchangeRequest struct {
diff --git a/exchange/exchangetest/firstpartydata-imp-ext-multiple-bidders.json b/exchange/exchangetest/firstpartydata-imp-ext-multiple-bidders.json
index df0e4e33..ff88274f 100644
--- a/exchange/exchangetest/firstpartydata-imp-ext-multiple-bidders.json
+++ b/exchange/exchangetest/firstpartydata-imp-ext-multiple-bidders.json
@@ -1,4 +1,5 @@
 {
+    "requestType": "openrtb2-web",
     "incomingRequest": {
         "ortbRequest": {
             "id": "some-request-id",
diff --git a/exchange/exchangetest/firstpartydata-imp-ext-multiple-prebid-bidders.json b/exchange/exchangetest/firstpartydata-imp-ext-multiple-prebid-bidders.json
index ea4a980c..0ecd3d5c 100644
--- a/exchange/exchangetest/firstpartydata-imp-ext-multiple-prebid-bidders.json
+++ b/exchange/exchangetest/firstpartydata-imp-ext-multiple-prebid-bidders.json
@@ -1,4 +1,5 @@
 {
+    "requestType": "openrtb2-web",
     "incomingRequest": {
         "ortbRequest": {
             "id": "some-request-id",
diff --git a/exchange/exchangetest/firstpartydata-imp-ext-one-bidder.json b/exchange/exchangetest/firstpartydata-imp-ext-one-bidder.json
index 7e2c4b9a..b8a22ffd 100644
--- a/exchange/exchangetest/firstpartydata-imp-ext-one-bidder.json
+++ b/exchange/exchangetest/firstpartydata-imp-ext-one-bidder.json
@@ -1,4 +1,5 @@
 {
+    "requestType": "openrtb2-web",
     "incomingRequest": {
         "ortbRequest": {
             "id": "some-request-id",
diff --git a/exchange/exchangetest/firstpartydata-imp-ext-one-prebid-bidder.json b/exchange/exchangetest/firstpartydata-imp-ext-one-prebid-bidder.json
index f1977426..e64fa787 100644
--- a/exchange/exchangetest/firstpartydata-imp-ext-one-prebid-bidder.json
+++ b/exchange/exchangetest/firstpartydata-imp-ext-one-prebid-bidder.json
@@ -1,4 +1,5 @@
 {
+    "requestType": "openrtb2-web",
     "incomingRequest": {
         "ortbRequest": {
             "id": "some-request-id",
diff --git a/exchange/exchangetest/firstpartydata-multibidder-config-invalid.json b/exchange/exchangetest/firstpartydata-multibidder-config-invalid.json
new file mode 100644
index 00000000..c7d9781e
--- /dev/null
+++ b/exchange/exchangetest/firstpartydata-multibidder-config-invalid.json
@@ -0,0 +1,67 @@
+{
+    "requestType": "openrtb2-web",
+    "incomingRequest": {
+        "ortbRequest": {
+            "id": "some-request-id",
+            "site": {
+                "page": "test.somepage.com"
+            },
+            "imp": [{
+                "id": "some-imp-id",
+                "banner": {
+                    "format": [{
+                        "w": 600,
+                        "h": 500
+                    }, {
+                        "w": 300,
+                        "h": 600
+                    }]
+                },
+                "ext": {
+                    "prebid": {
+                        "bidder": {
+                            "appnexus": {
+                                "placementId": 1
+                            }
+                        }
+                    }
+                }
+            }],
+            "ext":{
+                "prebid": {
+                    "data": {
+                        "bidders": ["appnexus"]
+                    },
+                    "bidderconfig": [
+                        {
+                            "bidders": ["appnexus"],
+                            "config": {
+                                "ortb2": {
+                                    "app": {
+                                        "domain": "bidconfig_domain1",
+                                        "page": "bidconfig_page1"
+                                    }
+                                }
+                            }
+                        },
+                        {
+                            "bidders": ["appnexus"],
+                            "config": {
+                                "ortb2": {
+                                    "app": {
+                                        "domain": "bidconfig_domain2",
+                                        "page": "bidconfig_page2"
+                                    }
+                                }
+                            }
+                        }
+                    ]
+                }
+            }
+        }
+    },
+    "outgoingRequests": {},
+    "response": {
+        "error": "multiple First Party Data bidder configs provided for bidder: appnexus"
+    }
+}
\ No newline at end of file
diff --git a/exchange/exchangetest/firstpartydata-multibidder-config-valid.json b/exchange/exchangetest/firstpartydata-multibidder-config-valid.json
new file mode 100644
index 00000000..d7c8946f
--- /dev/null
+++ b/exchange/exchangetest/firstpartydata-multibidder-config-valid.json
@@ -0,0 +1,163 @@
+{
+  "requestType": "openrtb2-web",
+  "incomingRequest": {
+    "ortbRequest": {
+      "id": "some-request-id",
+      "site": {
+        "page": "test.somepage.com"
+      },
+      "imp": [
+        {
+          "id": "some-imp-id",
+          "banner": {
+            "format": [
+              {
+                "w": 600,
+                "h": 500
+              },
+              {
+                "w": 300,
+                "h": 600
+              }
+            ]
+          },
+          "ext": {
+            "appnexus": {
+              "placementId": 1
+            },
+            "rubicon": {
+              "accountId": 1,
+              "siteId": 2,
+              "zoneId": 3
+            }
+          }
+        }
+      ],
+      "ext": {
+        "prebid": {
+          "data": {
+            "bidders": [
+              "appnexus",
+              "rubicon"
+            ]
+          },
+          "bidderconfig": [
+            {
+              "bidders": [
+                "appnexus"
+              ],
+              "config": {
+                "ortb2": {
+                  "site": {
+                    "domain": "fpd_appnexus_site_domain",
+                    "page": "fpd_appnexus_site_page"
+                  }
+                }
+              }
+            },
+            {
+              "bidders": [
+                "rubicon"
+              ],
+              "config": {
+                "ortb2": {
+                  "site": {
+                    "domain": "fpd_rubicon_site_domain",
+                    "page": "fpd_rubicon_site_page"
+                  }
+                }
+              }
+            }
+          ]
+        }
+      }
+    }
+  },
+  "outgoingRequests": {
+    "appnexus": {
+      "expectRequest": {
+        "ortbRequest": {
+          "id": "some-request-id",
+          "imp": [
+            {
+              "id": "some-imp-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 600,
+                    "h": 500
+                  },
+                  {
+                    "w": 300,
+                    "h": 600
+                  }
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "placementId": 1
+                }
+              }
+            }
+          ],
+          "site": {
+            "domain": "fpd_appnexus_site_domain",
+            "page": "fpd_appnexus_site_page"
+          },
+          "ext": {
+            "prebid": {
+              "data": {
+                "eidpermissions": null
+              }
+            }
+          }
+        },
+        "bidAdjustment": 1.0
+      }
+    },
+    "rubicon": {
+      "expectRequest": {
+        "ortbRequest": {
+          "id": "some-request-id",
+          "imp": [
+            {
+              "id": "some-imp-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 600,
+                    "h": 500
+                  },
+                  {
+                    "w": 300,
+                    "h": 600
+                  }
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "accountId": 1,
+                  "siteId": 2,
+                  "zoneId": 3
+                }
+              }
+            }
+          ],
+          "site": {
+            "domain": "fpd_rubicon_site_domain",
+            "page": "fpd_rubicon_site_page"
+          },
+          "ext": {
+            "prebid": {
+              "data": {
+                "eidpermissions": null
+              }
+            }
+          }
+        },
+        "bidAdjustment": 1.0
+      }
+    }
+  }
+
+}
\ No newline at end of file
diff --git a/exchange/targeting_test.go b/exchange/targeting_test.go
index de92eed3..f164ae01 100644
--- a/exchange/targeting_test.go
+++ b/exchange/targeting_test.go
@@ -108,7 +108,7 @@ func runTargetingAuction(t *testing.T, mockBids map[openrtb_ext.BidderName][]*op
 	}
 
 	auctionRequest := AuctionRequest{
-		BidRequest:             req,
+		BidRequestWrapper:      &openrtb_ext.RequestWrapper{BidRequest: req},
 		Account:                config.Account{},
 		UserSyncs:              &emptyUsersync{},
 		GDPRPermissionsBuilder: mockGDPRPermissionsBuilder,
diff --git a/exchange/utils_test.go b/exchange/utils_test.go
index 57905895..4c5f291e 100644
--- a/exchange/utils_test.go
+++ b/exchange/utils_test.go
@@ -464,14 +464,14 @@ func TestCleanOpenRTBRequests(t *testing.T) {
 		consentedVendors map[string]bool
 	}{
 		{
-			req:              AuctionRequest{BidRequest: getTestBuildRequest(t), UserSyncs: &emptyUsersync{}},
+			req:              AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: getTestBuildRequest(t)}, UserSyncs: &emptyUsersync{}},
 			bidReqAssertions: assertReq,
 			hasError:         false,
 			applyCOPPA:       true,
 			consentedVendors: map[string]bool{"appnexus": true},
 		},
 		{
-			req:              AuctionRequest{BidRequest: newAdapterAliasBidRequest(t), UserSyncs: &emptyUsersync{}},
+			req:              AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: newAdapterAliasBidRequest(t)}, UserSyncs: &emptyUsersync{}},
 			bidReqAssertions: assertReq,
 			hasError:         false,
 			applyCOPPA:       false,
@@ -525,22 +525,22 @@ func TestCleanOpenRTBRequestsWithFPD(t *testing.T) {
 	}{
 		{
 			description: "Pass valid FPD data for bidder not found in the request",
-			req:         AuctionRequest{BidRequest: getTestBuildRequest(t), UserSyncs: &emptyUsersync{}, FirstPartyData: fpd},
+			req:         AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: getTestBuildRequest(t)}, UserSyncs: &emptyUsersync{}, FirstPartyData: fpd},
 			fpdExpected: false,
 		},
 		{
 			description: "Pass valid FPD data for bidders specified in request",
-			req:         AuctionRequest{BidRequest: newAdapterAliasBidRequest(t), UserSyncs: &emptyUsersync{}, FirstPartyData: fpd},
+			req:         AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: newAdapterAliasBidRequest(t)}, UserSyncs: &emptyUsersync{}, FirstPartyData: fpd},
 			fpdExpected: true,
 		},
 		{
 			description: "Bidders specified in request but there is no fpd data for this bidder",
-			req:         AuctionRequest{BidRequest: newAdapterAliasBidRequest(t), UserSyncs: &emptyUsersync{}, FirstPartyData: make(map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData)},
+			req:         AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: newAdapterAliasBidRequest(t)}, UserSyncs: &emptyUsersync{}, FirstPartyData: make(map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData)},
 			fpdExpected: false,
 		},
 		{
 			description: "No FPD data passed",
-			req:         AuctionRequest{BidRequest: newAdapterAliasBidRequest(t), UserSyncs: &emptyUsersync{}, FirstPartyData: nil},
+			req:         AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: newAdapterAliasBidRequest(t)}, UserSyncs: &emptyUsersync{}, FirstPartyData: nil},
 			fpdExpected: false,
 		},
 	}
@@ -700,9 +700,9 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) {
 		}
 
 		auctionReq := AuctionRequest{
-			BidRequest: req,
-			UserSyncs:  &emptyUsersync{},
-			Account:    accountConfig,
+			BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req},
+			UserSyncs:         &emptyUsersync{},
+			Account:           accountConfig,
 		}
 
 		bidderToSyncerKey := map[string]string{}
@@ -764,8 +764,8 @@ func TestCleanOpenRTBRequestsCCPAErrors(t *testing.T) {
 		assert.NoError(t, err, test.description+":marshal_ext")
 
 		auctionReq := AuctionRequest{
-			BidRequest: req,
-			UserSyncs:  &emptyUsersync{},
+			BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req},
+			UserSyncs:         &emptyUsersync{},
 		}
 
 		privacyConfig := config.Privacy{
@@ -811,8 +811,8 @@ func TestCleanOpenRTBRequestsCOPPA(t *testing.T) {
 		req.Regs = &openrtb2.Regs{COPPA: test.coppa}
 
 		auctionReq := AuctionRequest{
-			BidRequest: req,
-			UserSyncs:  &emptyUsersync{},
+			BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req},
+			UserSyncs:         &emptyUsersync{},
 		}
 
 		bidderToSyncerKey := map[string]string{}
@@ -897,8 +897,8 @@ func TestCleanOpenRTBRequestsSChain(t *testing.T) {
 		}
 
 		auctionReq := AuctionRequest{
-			BidRequest: req,
-			UserSyncs:  &emptyUsersync{},
+			BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req},
+			UserSyncs:         &emptyUsersync{},
 		}
 
 		bidderToSyncerKey := map[string]string{}
@@ -954,8 +954,8 @@ func TestCleanOpenRTBRequestsBidderParams(t *testing.T) {
 		}
 
 		auctionReq := AuctionRequest{
-			BidRequest: req,
-			UserSyncs:  &emptyUsersync{},
+			BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req},
+			UserSyncs:         &emptyUsersync{},
 		}
 
 		bidderToSyncerKey := map[string]string{}
@@ -1629,8 +1629,8 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) {
 		req.Device.Lmt = test.lmt
 
 		auctionReq := AuctionRequest{
-			BidRequest: req,
-			UserSyncs:  &emptyUsersync{},
+			BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req},
+			UserSyncs:         &emptyUsersync{},
 		}
 
 		privacyConfig := config.Privacy{
@@ -1842,9 +1842,9 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 		}
 
 		auctionReq := AuctionRequest{
-			BidRequest: req,
-			UserSyncs:  &emptyUsersync{},
-			Account:    accountConfig,
+			BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req},
+			UserSyncs:         &emptyUsersync{},
+			Account:           accountConfig,
 		}
 
 		bidderToSyncerKey := map[string]string{}
@@ -1937,9 +1937,9 @@ func TestCleanOpenRTBRequestsGDPRBlockBidRequest(t *testing.T) {
 		}
 
 		auctionReq := AuctionRequest{
-			BidRequest: req,
-			UserSyncs:  &emptyUsersync{},
-			Account:    accountConfig,
+			BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req},
+			UserSyncs:         &emptyUsersync{},
+			Account:           accountConfig,
 		}
 
 		metricsMock := metrics.MetricsEngineMock{}
@@ -2521,8 +2521,8 @@ func TestCleanOpenRTBRequestsSChainMultipleBidders(t *testing.T) {
 	extRequest := unmarshaledExt
 
 	auctionReq := AuctionRequest{
-		BidRequest: req,
-		UserSyncs:  &emptyUsersync{},
+		BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req},
+		UserSyncs:         &emptyUsersync{},
 	}
 	bidderToSyncerKey := map[string]string{}
 	metrics := metrics.MetricsEngineMock{}