Repository: prebid/prebid-server
Pull request #3113 (2023-09-20 08:17:12)

make case insensitive comparison in auction request
We need to make case insensitive comparison in the request while in the response we should return the same bidder name(with whatever case) from the request.
By doing case insensitive comparison in the request, we are not making any changes in our current auction flow. Our current auction flow will work the same way as it is today.






Patch:
diff --git a/adapters/infoawarebidder.go b/adapters/infoawarebidder.go
index 321a87a3..538a46ae 100644
--- a/adapters/infoawarebidder.go
+++ b/adapters/infoawarebidder.go
@@ -13,7 +13,7 @@ import (
 // media types defined in the static/bidder-info/{bidder}.yaml file.
 //
 // It adjusts incoming requests in the following ways:
-//  1. If App or Site traffic is not supported by the info file, then requests from
+//  1. If App, Site or DOOH traffic is not supported by the info file, then requests from
 //     those sources will be rejected before the delegate is called.
 //  2. If a given MediaType is not supported for the platform, then it will be set
 //     to nil before the request is forwarded to the delegate.
@@ -24,7 +24,7 @@ type InfoAwareBidder struct {
 	info parsedBidderInfo
 }
 
-// BuildInfoAwareBidder wraps a bidder to enforce site, app, and media type support.
+// BuildInfoAwareBidder wraps a bidder to enforce inventory {site, app, dooh} and media type support.
 func BuildInfoAwareBidder(bidder Bidder, info config.BidderInfo) Bidder {
 	return &InfoAwareBidder{
 		Bidder: bidder,
@@ -47,6 +47,12 @@ func (i *InfoAwareBidder) MakeRequests(request *openrtb2.BidRequest, reqInfo *Ex
 		}
 		allowedMediaTypes = i.info.app
 	}
+	if request.DOOH != nil {
+		if !i.info.dooh.enabled {
+			return nil, []error{&errortypes.Warning{Message: "this bidder does not support dooh requests"}}
+		}
+		allowedMediaTypes = i.info.dooh
+	}
 
 	// Filtering imps is quite expensive (array filter with large, non-pointer elements)... but should be rare,
 	// because it only happens if the publisher makes a really bad request.
@@ -136,6 +142,7 @@ func filterImps(imps []openrtb2.Imp, numToFilter int) ([]openrtb2.Imp, []error)
 type parsedBidderInfo struct {
 	app  parsedSupports
 	site parsedSupports
+	dooh parsedSupports
 }
 
 type parsedSupports struct {
@@ -148,13 +155,22 @@ type parsedSupports struct {
 
 func parseBidderInfo(info config.BidderInfo) parsedBidderInfo {
 	var parsedInfo parsedBidderInfo
-	if info.Capabilities != nil && info.Capabilities.App != nil {
+
+	if info.Capabilities == nil {
+		return parsedInfo
+	}
+
+	if info.Capabilities.App != nil {
 		parsedInfo.app.enabled = true
 		parsedInfo.app.banner, parsedInfo.app.video, parsedInfo.app.audio, parsedInfo.app.native = parseAllowedTypes(info.Capabilities.App.MediaTypes)
 	}
-	if info.Capabilities != nil && info.Capabilities.Site != nil {
+	if info.Capabilities.Site != nil {
 		parsedInfo.site.enabled = true
 		parsedInfo.site.banner, parsedInfo.site.video, parsedInfo.site.audio, parsedInfo.site.native = parseAllowedTypes(info.Capabilities.Site.MediaTypes)
 	}
+	if info.Capabilities.DOOH != nil {
+		parsedInfo.dooh.enabled = true
+		parsedInfo.dooh.banner, parsedInfo.dooh.video, parsedInfo.dooh.audio, parsedInfo.dooh.native = parseAllowedTypes(info.Capabilities.DOOH.MediaTypes)
+	}
 	return parsedInfo
 }
diff --git a/config/account.go b/config/account.go
index e5265072..4b0ceeef 100644
--- a/config/account.go
+++ b/config/account.go
@@ -20,6 +20,7 @@ const (
 	ChannelApp   ChannelType = "app"
 	ChannelVideo ChannelType = "video"
 	ChannelWeb   ChannelType = "web"
+	ChannelDOOH  ChannelType = "dooh"
 )
 
 // Account represents a publisher account configuration
@@ -251,6 +252,7 @@ type AccountChannel struct {
 	App   *bool `mapstructure:"app" json:"app,omitempty"`
 	Video *bool `mapstructure:"video" json:"video,omitempty"`
 	Web   *bool `mapstructure:"web" json:"web,omitempty"`
+	DOOH  *bool `mapstructure:"dooh" json:"dooh,omitempty"`
 }
 
 // GetByChannelType looks up the account integration enabled setting for the specified channel type
@@ -266,6 +268,8 @@ func (a *AccountChannel) GetByChannelType(channelType ChannelType) *bool {
 		channelEnabled = a.Video
 	case ChannelWeb:
 		channelEnabled = a.Web
+	case ChannelDOOH:
+		channelEnabled = a.DOOH
 	}
 
 	return channelEnabled
@@ -296,7 +300,7 @@ func (m AccountModules) ModuleConfig(id string) (json.RawMessage, error) {
 }
 
 func (a *AccountChannel) IsSet() bool {
-	return a.AMP != nil || a.App != nil || a.Video != nil || a.Web != nil
+	return a.AMP != nil || a.App != nil || a.Video != nil || a.Web != nil || a.DOOH != nil
 }
 
 type AccountPrivacy struct {
diff --git a/config/bidderinfo.go b/config/bidderinfo.go
index 96d6fd15..0027e4d2 100644
--- a/config/bidderinfo.go
+++ b/config/bidderinfo.go
@@ -78,6 +78,7 @@ type MaintainerInfo struct {
 type CapabilitiesInfo struct {
 	App  *PlatformInfo `yaml:"app" mapstructure:"app"`
 	Site *PlatformInfo `yaml:"site" mapstructure:"site"`
+	DOOH *PlatformInfo `yaml:"dooh" mapstructure:"dooh"`
 }
 
 // PlatformInfo specifies the supported media types for a bidder.
@@ -461,7 +462,9 @@ func validateAliasCapabilities(aliasBidderInfo BidderInfo, infos BidderInfos, bi
 			return fmt.Errorf("capabilities for alias: %s should be a subset of capabilities for parent bidder: %s", bidderName, aliasBidderInfo.AliasOf)
 		}
 
-		if (aliasBidderInfo.Capabilities.App != nil && parentBidder.Capabilities.App == nil) || (aliasBidderInfo.Capabilities.Site != nil && parentBidder.Capabilities.Site == nil) {
+		if (aliasBidderInfo.Capabilities.App != nil && parentBidder.Capabilities.App == nil) ||
+			(aliasBidderInfo.Capabilities.Site != nil && parentBidder.Capabilities.Site == nil) ||
+			(aliasBidderInfo.Capabilities.DOOH != nil && parentBidder.Capabilities.DOOH == nil) {
 			return fmt.Errorf("capabilities for alias: %s should be a subset of capabilities for parent bidder: %s", bidderName, aliasBidderInfo.AliasOf)
 		}
 
@@ -476,6 +479,12 @@ func validateAliasCapabilities(aliasBidderInfo BidderInfo, infos BidderInfos, bi
 				return err
 			}
 		}
+
+		if aliasBidderInfo.Capabilities.DOOH != nil && parentBidder.Capabilities.DOOH != nil {
+			if err := isAliasPlatformInfoSubsetOfParent(*parentBidder.Capabilities.DOOH, *aliasBidderInfo.Capabilities.DOOH, bidderName, aliasBidderInfo.AliasOf); err != nil {
+				return err
+			}
+		}
 	}
 
 	return nil
@@ -501,8 +510,8 @@ func validateCapabilities(info *CapabilitiesInfo, bidderName string) error {
 		return fmt.Errorf("missing required field: capabilities for adapter: %s", bidderName)
 	}
 
-	if info.App == nil && info.Site == nil {
-		return fmt.Errorf("at least one of capabilities.site or capabilities.app must exist for adapter: %s", bidderName)
+	if info.App == nil && info.Site == nil && info.DOOH == nil {
+		return fmt.Errorf("at least one of capabilities.site, capabilities.app, or capabilities.dooh must exist for adapter: %s", bidderName)
 	}
 
 	if info.App != nil {
@@ -513,9 +522,16 @@ func validateCapabilities(info *CapabilitiesInfo, bidderName string) error {
 
 	if info.Site != nil {
 		if err := validatePlatformInfo(info.Site); err != nil {
-			return fmt.Errorf("capabilities.site failed validation: %v, for adapter: %s", err, bidderName)
+			return fmt.Errorf("capabilities.site failed validation: %v for adapter: %s", err, bidderName)
 		}
 	}
+
+	if info.DOOH != nil {
+		if err := validatePlatformInfo(info.DOOH); err != nil {
+			return fmt.Errorf("capabilities.dooh failed validation: %v for adapter: %s", err, bidderName)
+		}
+	}
+
 	return nil
 }
 
diff --git a/endpoints/info/bidders_detail.go b/endpoints/info/bidders_detail.go
index 1446e3ac..9b8d4268 100644
--- a/endpoints/info/bidders_detail.go
+++ b/endpoints/info/bidders_detail.go
@@ -122,6 +122,7 @@ type maintainer struct {
 type capabilities struct {
 	App  *platform `json:"app,omitempty"`
 	Site *platform `json:"site,omitempty"`
+	DOOH *platform `json:"dooh,omitempty"`
 }
 
 type platform struct {
@@ -157,6 +158,12 @@ func mapDetailFromConfig(c config.BidderInfo) bidderDetail {
 					MediaTypes: mapMediaTypes(c.Capabilities.Site.MediaTypes),
 				}
 			}
+
+			if c.Capabilities.DOOH != nil {
+				bidderDetail.Capabilities.DOOH = &platform{
+					MediaTypes: mapMediaTypes(c.Capabilities.DOOH.MediaTypes),
+				}
+			}
 		}
 	} else {
 		bidderDetail.Status = statusDisabled
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index aeb5c20e..f419ca34 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -69,13 +69,16 @@ var (
 )
 
 var accountIdSearchPath = [...]struct {
-	isApp bool
-	key   []string
+	isApp  bool
+	isDOOH bool
+	key    []string
 }{
-	{true, []string{"app", "publisher", "ext", openrtb_ext.PrebidExtKey, "parentAccount"}},
-	{true, []string{"app", "publisher", "id"}},
-	{false, []string{"site", "publisher", "ext", openrtb_ext.PrebidExtKey, "parentAccount"}},
-	{false, []string{"site", "publisher", "id"}},
+	{true, false, []string{"app", "publisher", "ext", openrtb_ext.PrebidExtKey, "parentAccount"}},
+	{true, false, []string{"app", "publisher", "id"}},
+	{false, false, []string{"site", "publisher", "ext", openrtb_ext.PrebidExtKey, "parentAccount"}},
+	{false, false, []string{"site", "publisher", "id"}},
+	{false, true, []string{"dooh", "publisher", "ext", openrtb_ext.PrebidExtKey, "parentAccount"}},
+	{false, true, []string{"dooh", "publisher", "id"}},
 }
 
 func NewEndpoint(
@@ -457,12 +460,16 @@ func (deps *endpointDeps) parseRequest(httpRequest *http.Request, labels *metric
 		return
 	}
 
-	accountId, isAppReq, errs := getAccountIdFromRawRequest(hasStoredBidRequest, storedRequests[storedBidRequestId], requestJson)
+	accountId, isAppReq, isDOOHReq, errs := getAccountIdFromRawRequest(hasStoredBidRequest, storedRequests[storedBidRequestId], requestJson)
 	// fill labels here in order to pass correct metrics in case of errors
 	if isAppReq {
 		labels.Source = metrics.DemandApp
 		labels.RType = metrics.ReqTypeORTB2App
 		labels.PubID = accountId
+	} else if isDOOHReq {
+		labels.Source = metrics.DemandDOOH
+		labels.RType = metrics.ReqTypeORTB2DOOH
+		labels.PubID = accountId
 	} else { // is Site request
 		labels.Source = metrics.DemandWeb
 		labels.PubID = accountId
@@ -804,8 +811,8 @@ func (deps *endpointDeps) validateRequest(req *openrtb_ext.RequestWrapper, isAmp
 		return []error{err}
 	}
 
-	if (req.Site == nil && req.App == nil) || (req.Site != nil && req.App != nil) {
-		return append(errL, errors.New("request.site or request.app must be defined, but not both."))
+	if err := validateExactlyOneInventoryType(req); err != nil {
+		return []error{err}
 	}
 
 	if errs := validateRequestExt(req); len(errs) != 0 {
@@ -823,6 +830,9 @@ func (deps *endpointDeps) validateRequest(req *openrtb_ext.RequestWrapper, isAmp
 		return append(errL, err)
 	}
 
+	if err := deps.validateDOOH(req); err != nil {
+		return append(errL, err)
+	}
 	var gpp gpplib.GppContainer
 	if req.BidRequest.Regs != nil && len(req.BidRequest.Regs.GPP) > 0 {
 		gpp, err = gpplib.Parse(req.BidRequest.Regs.GPP)
@@ -1510,12 +1520,12 @@ func (deps *endpointDeps) validateImpExt(imp *openrtb_ext.ImpWrapper, aliases ma
 	errL := []error{}
 
 	for bidder, ext := range prebid.Bidder {
-		coreBidder := bidder
+		coreBidder, _ := openrtb_ext.NormalizeBidderName(bidder)
 		if tmp, isAlias := aliases[bidder]; isAlias {
-			coreBidder = tmp
+			coreBidder = openrtb_ext.BidderName(tmp)
 		}
 
-		if coreBidderNormalized, isValid := deps.bidderMap[coreBidder]; isValid {
+		if coreBidderNormalized, isValid := deps.bidderMap[coreBidder.String()]; isValid {
 			if err := deps.paramsValidator.Validate(coreBidderNormalized, ext); err != nil {
 				return []error{fmt.Errorf("request.imp[%d].ext.prebid.bidder.%s failed validation.\n%v", impIndex, bidder, err)}
 			}
@@ -1575,18 +1585,21 @@ func (deps *endpointDeps) parseBidExt(req *openrtb_ext.RequestWrapper) error {
 }
 
 func (deps *endpointDeps) validateAliases(aliases map[string]string) error {
-	for alias, coreBidder := range aliases {
-		if _, isCoreBidderDisabled := deps.disabledBidders[coreBidder]; isCoreBidderDisabled {
-			return fmt.Errorf("request.ext.prebid.aliases.%s refers to disabled bidder: %s", alias, coreBidder)
+	for alias, bidderName := range aliases {
+		normalisedBidderName, _ := openrtb_ext.NormalizeBidderName(bidderName)
+		coreBidderName := normalisedBidderName.String()
+		if _, isCoreBidderDisabled := deps.disabledBidders[coreBidderName]; isCoreBidderDisabled {
+			return fmt.Errorf("request.ext.prebid.aliases.%s refers to disabled bidder: %s", alias, bidderName)
 		}
 
-		if _, isCoreBidder := deps.bidderMap[coreBidder]; !isCoreBidder {
-			return fmt.Errorf("request.ext.prebid.aliases.%s refers to unknown bidder: %s", alias, coreBidder)
+		if _, isCoreBidder := deps.bidderMap[coreBidderName]; !isCoreBidder {
+			return fmt.Errorf("request.ext.prebid.aliases.%s refers to unknown bidder: %s", alias, bidderName)
 		}
 
-		if alias == coreBidder {
+		if alias == coreBidderName {
 			return fmt.Errorf("request.ext.prebid.aliases.%s defines a no-op alias. Choose a different alias, or remove this entry.", alias)
 		}
+		aliases[alias] = coreBidderName
 	}
 	return nil
 }
@@ -1747,6 +1760,18 @@ func (deps *endpointDeps) validateApp(req *openrtb_ext.RequestWrapper) error {
 	return err
 }
 
+func (deps *endpointDeps) validateDOOH(req *openrtb_ext.RequestWrapper) error {
+	if req.DOOH == nil {
+		return nil
+	}
+
+	if req.DOOH.ID == "" && len(req.DOOH.VenueType) == 0 {
+		return errors.New("request.dooh should include at least one of request.dooh.id or request.dooh.venuetype.")
+	}
+
+	return nil
+}
+
 func (deps *endpointDeps) validateUser(req *openrtb_ext.RequestWrapper, aliases map[string]string, gpp gpplib.GppContainer) []error {
 	var errL []error
 
@@ -1872,6 +1897,30 @@ func validateDevice(device *openrtb2.Device) error {
 	return nil
 }
 
+func validateExactlyOneInventoryType(reqWrapper *openrtb_ext.RequestWrapper) error {
+
+	// Prep for mutual exclusion check
+	invTypeNumMatches := 0
+	if reqWrapper.Site != nil {
+		invTypeNumMatches++
+	}
+	if reqWrapper.App != nil {
+		invTypeNumMatches++
+	}
+	if reqWrapper.DOOH != nil {
+		invTypeNumMatches++
+	}
+
+	if invTypeNumMatches == 0 {
+		return errors.New("One of request.site or request.app or request.dooh must be defined")
+	} else if invTypeNumMatches >= 2 {
+		return errors.New("No more than one of request.site or request.app or request.dooh can be defined")
+	} else {
+		return nil
+	}
+
+}
+
 func validateOrFillChannel(reqWrapper *openrtb_ext.RequestWrapper, isAmp bool) error {
 	requestExt, err := reqWrapper.GetRequestExt()
 	if err != nil {
@@ -1934,9 +1983,9 @@ func (deps *endpointDeps) setFieldsImplicitly(httpReq *http.Request, r *openrtb_
 
 	setDeviceImplicitly(httpReq, r, deps.privateNetworkIPValidator)
 
-	// Per the OpenRTB spec: A bid request must not contain both a Site and an App object. If neither are
-	// present, we'll assume it's a site request.
-	if r.App == nil {
+	// Per the OpenRTB spec: A bid request must not contain more than one of Site|App|DOOH
+	// Assume it's a site request if it's not declared as one of the other values
+	if r.App == nil && r.DOOH == nil {
 		setSiteImplicitly(httpReq, r)
 	}
 
@@ -2325,43 +2374,43 @@ func getAccountID(pub *openrtb2.Publisher) string {
 	return metrics.PublisherUnknown
 }
 
-func getAccountIdFromRawRequest(hasStoredRequest bool, storedRequest json.RawMessage, originalRequest []byte) (string, bool, []error) {
+func getAccountIdFromRawRequest(hasStoredRequest bool, storedRequest json.RawMessage, originalRequest []byte) (string, bool, bool, []error) {
 	request := originalRequest
 	if hasStoredRequest {
 		request = storedRequest
 	}
 
-	accountId, isAppReq, err := searchAccountId(request)
+	accountId, isAppReq, isDOOHReq, err := searchAccountId(request)
 	if err != nil {
-		return "", isAppReq, []error{err}
+		return "", isAppReq, isDOOHReq, []error{err}
 	}
 
 	// In case the stored request did not have account data we specifically search it in the original request
 	if accountId == "" && hasStoredRequest {
-		accountId, _, err = searchAccountId(originalRequest)
+		accountId, _, _, err = searchAccountId(originalRequest)
 		if err != nil {
-			return "", isAppReq, []error{err}
+			return "", isAppReq, isDOOHReq, []error{err}
 		}
 	}
 
 	if accountId == "" {
-		return metrics.PublisherUnknown, isAppReq, nil
+		return metrics.PublisherUnknown, isAppReq, isDOOHReq, nil
 	}
 
-	return accountId, isAppReq, nil
+	return accountId, isAppReq, isDOOHReq, nil
 }
 
-func searchAccountId(request []byte) (string, bool, error) {
+func searchAccountId(request []byte) (string, bool, bool, error) {
 	for _, path := range accountIdSearchPath {
 		accountId, exists, err := getStringValueFromRequest(request, path.key)
 		if err != nil {
-			return "", path.isApp, err
+			return "", path.isApp, path.isDOOH, err
 		}
 		if exists {
-			return accountId, path.isApp, nil
+			return accountId, path.isApp, path.isDOOH, nil
 		}
 	}
-	return "", false, nil
+	return "", false, false, nil
 }
 
 func getStringValueFromRequest(request []byte, key []string) (string, bool, error) {
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/no-site-or-app.json b/endpoints/openrtb2/sample-requests/invalid-whole/no-site-or-app-or-dooh.json
similarity index 85%
rename from endpoints/openrtb2/sample-requests/invalid-whole/no-site-or-app.json
rename to endpoints/openrtb2/sample-requests/invalid-whole/no-site-or-app-or-dooh.json
index 8ec2273e..c883979c 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/no-site-or-app.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/no-site-or-app-or-dooh.json
@@ -1,5 +1,5 @@
 {
-  "description": "Request does not come with site field nor app field",
+  "description": "Request does not come with site nor app nor dooh field",
   "mockBidRequest": {
     "id": "req-id",
     "imp": [
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/site-app-both.json b/endpoints/openrtb2/sample-requests/invalid-whole/site-app-both.json
index 5bc3054c..255ea59e 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/site-app-both.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/site-app-both.json
@@ -1,5 +1,5 @@
 {
-  "description": "Bid request comes with both site and app fields, it should only come with one or the other",
+  "description": "Bid request comes with both site and app fields, it should only come with one",
   "mockBidRequest": {
     "id": "req-id",
     "site": {
@@ -23,5 +23,5 @@
     ]
   },
   "expectedReturnCode": 400,
-  "expectedErrorMessage": "Invalid request: request.site or request.app must be defined, but not both.\n"
+  "expectedErrorMessage": "Invalid request: No more than one of request.site or request.app or request.dooh can be defined\n"
 }
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/site-dooh-both.json b/endpoints/openrtb2/sample-requests/invalid-whole/site-dooh-both.json
new file mode 100644
index 00000000..d4760d12
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/site-dooh-both.json
@@ -0,0 +1,27 @@
+{
+  "description": "Bid request comes with both site and dooh fields, it should only come with one",
+  "mockBidRequest": {
+    "id": "req-id",
+    "site": {
+      "page": "test.mysite.com"
+    },
+    "dooh": {},
+    "imp": [
+      {
+        "id": "imp-id",
+        "video": {
+          "mimes": [
+            "video/mp4"
+            ]
+        },
+        "ext": {
+          "appnexus": {
+            "placementId": 12883451
+          }
+        }
+      }
+    ]
+  },
+  "expectedReturnCode": 400,
+  "expectedErrorMessage": "Invalid request: No more than one of request.site or request.app or request.dooh can be defined\n"
+}
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/all-ext-case-insensitive.json b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/all-ext-case-insensitive.json
new file mode 100644
index 00000000..20997076
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/all-ext-case-insensitive.json
@@ -0,0 +1,140 @@
+{
+  "description": "This demonstrates all extension in case insensitive.",
+  "config": {
+    "mockBidders": [
+      {
+        "bidderName": "appnexus",
+        "currency": "USD",
+        "price": 1.00
+      },
+      {
+        "bidderName": "rubicon",
+        "currency": "USD",
+        "price": 1.00
+      }
+    ]
+  },
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "site": {
+      "page": "prebid.org"
+    },
+    "user": {
+      "ext": {
+        "consent": "gdpr-consent-string",
+        "prebid": {
+          "buyeruids": {
+            "appnexus": "override-appnexus-id-in-cookie"
+          }
+        }
+      }
+    },
+    "regs": {
+      "ext": {
+        "gdpr": 1,
+        "us_privacy": "1NYN"
+      }
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "appnexus": {
+            "placementId": 12883451
+          },
+          "districtm": {
+            "placementId": 105
+          },
+          "rubicon": {
+            "accountId": 1001,
+            "siteId": 113932,
+            "zoneId": 535510
+          }
+        }
+      }
+    ],
+    "tmax": 500,
+    "ext": {
+      "prebid": {
+        "aliases": {
+          "districtm": "Appnexus"
+        },
+        "bidadjustmentfactors": {
+          "appnexus": 1.01,
+          "districtm": 0.98,
+          "rubicon": 0.99
+        },
+        "cache": {
+          "bids": {}
+        },
+        "channel": {
+          "name": "video",
+          "version": "1.0"
+        },
+        "targeting": {
+          "includewinners": false,
+          "pricegranularity": {
+            "precision": 2,
+            "ranges": [
+              {
+                "max": 20,
+                "increment": 0.10
+              }
+            ]
+          }
+        }
+      }
+    }
+  },
+  "expectedBidResponse": {
+    "id": "some-request-id",
+    "seatbid": [
+      {
+        "bid": [
+          {
+            "id": "appnexus-bid",
+            "impid": "some-impression-id",
+            "price": 1.01
+          }
+        ],
+        "seat": "appnexus"
+      },
+      {
+        "bid": [
+          {
+            "id": "appnexus-bid",
+            "impid": "some-impression-id",
+            "price": 0.98
+          }
+        ],
+        "seat": "districtm"
+      },
+      {
+        "bid": [
+          {
+            "id": "rubicon-bid",
+            "impid": "some-impression-id",
+            "price": 0.99
+          }
+        ],
+        "seat": "rubicon"
+      }
+    ],
+    "bidid": "test bid id",
+    "cur": "USD",
+    "nbr": 0
+  },
+  "expectedReturnCode": 200
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/all-ext.json b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/all-ext.json
index 8f64fd2a..02dc6160 100644
--- a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/all-ext.json
+++ b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/all-ext.json
@@ -2,8 +2,16 @@
   "description": "This demonstrates all of the OpenRTB extensions supported by Prebid Server. Very few requests will need all of these at once.",
   "config": {
     "mockBidders": [
-      {"bidderName": "appnexus", "currency": "USD", "price": 1.00},
-      {"bidderName": "rubicon", "currency": "USD", "price": 1.00}
+      {
+        "bidderName": "appnexus",
+        "currency": "USD",
+        "price": 1.00
+      },
+      {
+        "bidderName": "rubicon",
+        "currency": "USD",
+        "price": 1.00
+      }
     ]
   },
   "mockBidRequest": {
@@ -91,42 +99,42 @@
     }
   },
   "expectedBidResponse": {
-      "id":"some-request-id",
-      "seatbid": [
-        {
-          "bid": [
-            {
-              "id": "appnexus-bid",
-              "impid": "some-impression-id",
-              "price": 1.01
-            }
-          ],
-          "seat": "appnexus"
-        },
-        {
-          "bid": [
-            {
-              "id": "appnexus-bid",
-              "impid": "some-impression-id",
-              "price": 0.98
-            }
-          ],
-          "seat": "districtm"
-        },
-        {
-          "bid": [
-            {
-              "id": "rubicon-bid",
-              "impid": "some-impression-id",
-              "price": 0.99
-            }
-          ],
-          "seat": "rubicon"
-        }
-      ],
-      "bidid":"test bid id",
-      "cur":"USD",
-      "nbr":0
+    "id": "some-request-id",
+    "seatbid": [
+      {
+        "bid": [
+          {
+            "id": "appnexus-bid",
+            "impid": "some-impression-id",
+            "price": 1.01
+          }
+        ],
+        "seat": "appnexus"
+      },
+      {
+        "bid": [
+          {
+            "id": "appnexus-bid",
+            "impid": "some-impression-id",
+            "price": 0.98
+          }
+        ],
+        "seat": "districtm"
+      },
+      {
+        "bid": [
+          {
+            "id": "rubicon-bid",
+            "impid": "some-impression-id",
+            "price": 0.99
+          }
+        ],
+        "seat": "rubicon"
+      }
+    ],
+    "bidid": "test bid id",
+    "cur": "USD",
+    "nbr": 0
   },
   "expectedReturnCode": 200
-}
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/dooh.json b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/dooh.json
new file mode 100644
index 00000000..faa9a7f6
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/dooh.json
@@ -0,0 +1,53 @@
+{
+  "description": "Simple DOOH request",
+  "config": {
+    "mockBidders": [
+      {"bidderName": "appnexus", "currency": "USD", "price": 0.00}
+    ]
+  },
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "dooh": {
+      "id": "12345"
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "banner": {
+          "format": [
+            {
+              "w": 1920,
+              "h": 1080
+            }
+          ]
+        },
+        "ext": {
+          "appnexus": {
+            "placementId": 12883451
+          }
+        }
+      }
+    ],
+    "tmax": 500,
+    "ext": {}
+  },
+  "expectedBidResponse": {
+    "id": "some-request-id",
+    "seatbid": [
+      {
+        "bid": [
+          {
+            "id": "appnexus-bid",
+            "impid": "some-impression-id",
+            "price": 0
+          }
+        ],
+        "seat": "appnexus"
+      }
+    ],
+    "bidid": "test bid id",
+    "cur": "USD",
+    "nbr": 0
+  },
+  "expectedReturnCode": 200
+}
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/simple-case-insensitive.json b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/simple-case-insensitive.json
new file mode 100644
index 00000000..9b66a59d
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/simple-case-insensitive.json
@@ -0,0 +1,61 @@
+{
+    "description": "Simple request - bidder case insensitive",
+    "config": {
+        "mockBidders": [
+            {
+                "bidderName": "appnexus",
+                "currency": "USD",
+                "price": 0.00
+            }
+        ]
+    },
+    "mockBidRequest": {
+        "id": "some-request-id",
+        "site": {
+            "page": "prebid.org"
+        },
+        "imp": [
+            {
+                "id": "some-impression-id",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                },
+                "ext": {
+                    "Appnexus": {
+                        "placementId": 12883451
+                    }
+                }
+            }
+        ],
+        "tmax": 500,
+        "ext": {}
+    },
+    "expectedBidResponse": {
+        "id": "some-request-id",
+        "seatbid": [
+            {
+                "bid": [
+                    {
+                        "id": "appnexus-bid",
+                        "impid": "some-impression-id",
+                        "price": 0
+                    }
+                ],
+                "seat": "Appnexus"
+            }
+        ],
+        "bidid": "test bid id",
+        "cur": "USD",
+        "nbr": 0
+    },
+    "expectedReturnCode": 200
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/simple.json b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/simple.json
index ba9079d4..5a7dbd20 100644
--- a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/simple.json
+++ b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/simple.json
@@ -2,7 +2,11 @@
   "description": "Simple request",
   "config": {
     "mockBidders": [
-      {"bidderName": "appnexus", "currency": "USD", "price": 0.00}
+      {
+        "bidderName": "appnexus",
+        "currency": "USD",
+        "price": 0.00
+      }
     ]
   },
   "mockBidRequest": {
@@ -36,22 +40,22 @@
     "ext": {}
   },
   "expectedBidResponse": {
-      "id":"some-request-id",
-      "seatbid": [
-        {
-          "bid": [
-            {
-              "id": "appnexus-bid",
-              "impid": "some-impression-id",
-              "price": 0
-            }
-          ],
-          "seat": "appnexus"
-        }
-      ],
-      "bidid":"test bid id",
-      "cur":"USD",
-      "nbr":0
+    "id": "some-request-id",
+    "seatbid": [
+      {
+        "bid": [
+          {
+            "id": "appnexus-bid",
+            "impid": "some-impression-id",
+            "price": 0
+          }
+        ],
+        "seat": "appnexus"
+      }
+    ],
+    "bidid": "test bid id",
+    "cur": "USD",
+    "nbr": 0
   },
   "expectedReturnCode": 200
-}
+}
\ No newline at end of file
diff --git a/errortypes/code.go b/errortypes/code.go
index c68eb196..ef8287f2 100644
--- a/errortypes/code.go
+++ b/errortypes/code.go
@@ -14,6 +14,7 @@ const (
 	NoConversionRateErrorCode
 	MalformedAcctErrorCode
 	ModuleRejectionErrorCode
+	TmaxTimeoutErrorCode
 )
 
 // Defines numeric codes for well-known warnings.
diff --git a/errortypes/errortypes.go b/errortypes/errortypes.go
index aff04822..01a5cca4 100644
--- a/errortypes/errortypes.go
+++ b/errortypes/errortypes.go
@@ -20,6 +20,25 @@ func (err *Timeout) Severity() Severity {
 	return SeverityFatal
 }
 
+// TmaxTimeout should be used to flag that remaining tmax duration is not enough to get response from bidder
+//
+// TmaxTimeout will not be written to the app log, since it's not an actionable item for the Prebid Server hosts.
+type TmaxTimeout struct {
+	Message string
+}
+
+func (err *TmaxTimeout) Error() string {
+	return err.Message
+}
+
+func (err *TmaxTimeout) Code() int {
+	return TmaxTimeoutErrorCode
+}
+
+func (err *TmaxTimeout) Severity() Severity {
+	return SeverityFatal
+}
+
 // BadInput should be used when returning errors which are caused by bad input.
 // It should _not_ be used if the error is a server-side issue (e.g. failed to send the external request).
 //
diff --git a/exchange/bidder.go b/exchange/bidder.go
index 838c46f7..dfdb7cdf 100644
--- a/exchange/bidder.go
+++ b/exchange/bidder.go
@@ -88,8 +88,6 @@ const (
 	Gzip string = "GZIP"
 )
 
-var errTmaxTimeout = errors.New("exceeded tmax duration")
-
 // AdaptBidder converts an adapters.Bidder into an exchange.AdaptedBidder.
 //
 // The name refers to the "Adapter" architecture pattern, and should not be confused with a Prebid "Adapter"
@@ -551,7 +549,7 @@ func (bidder *bidderAdapter) doRequestImpl(ctx context.Context, req *adapters.Re
 			bidder.me.RecordTMaxTimeout()
 			return &httpCallInfo{
 				request: req,
-				err:     errTmaxTimeout,
+				err:     &errortypes.TmaxTimeout{Message: "exceeded tmax duration"},
 			}
 		}
 	}
diff --git a/exchange/exchange.go b/exchange/exchange.go
index 136f1040..7544b4a7 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -844,6 +844,8 @@ func errorsToMetric(errs []error) map[metrics.AdapterError]struct{} {
 			ret[metrics.AdapterErrorFailedToRequestBids] = s
 		case errortypes.AlternateBidderCodeWarningCode:
 			ret[metrics.AdapterErrorValidation] = s
+		case errortypes.TmaxTimeoutErrorCode:
+			ret[metrics.AdapterErrorTmaxTimeout] = s
 		default:
 			ret[metrics.AdapterErrorUnknown] = s
 		}
diff --git a/exchange/utils.go b/exchange/utils.go
index 0becca21..0a38e487 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -29,10 +29,11 @@ import (
 )
 
 var channelTypeMap = map[metrics.RequestType]config.ChannelType{
-	metrics.ReqTypeAMP:      config.ChannelAMP,
-	metrics.ReqTypeORTB2App: config.ChannelApp,
-	metrics.ReqTypeVideo:    config.ChannelVideo,
-	metrics.ReqTypeORTB2Web: config.ChannelWeb,
+	metrics.ReqTypeAMP:       config.ChannelAMP,
+	metrics.ReqTypeORTB2App:  config.ChannelApp,
+	metrics.ReqTypeVideo:     config.ChannelVideo,
+	metrics.ReqTypeORTB2Web:  config.ChannelWeb,
+	metrics.ReqTypeORTB2DOOH: config.ChannelDOOH,
 }
 
 const unknownBidder string = ""
@@ -761,10 +762,11 @@ func setUserExtWithCopy(request *openrtb2.BidRequest, userExtJSON json.RawMessag
 
 // resolveBidder returns the known BidderName associated with bidder, if bidder is an alias. If it's not an alias, the bidder is returned.
 func resolveBidder(bidder string, aliases map[string]string) openrtb_ext.BidderName {
+	normalisedBidderName, _ := openrtb_ext.NormalizeBidderName(bidder)
 	if coreBidder, ok := aliases[bidder]; ok {
 		return openrtb_ext.BidderName(coreBidder)
 	}
-	return openrtb_ext.BidderName(bidder)
+	return normalisedBidderName
 }
 
 // parseAliases parses the aliases from the BidRequest
diff --git a/metrics/metrics.go b/metrics/metrics.go
index c30f8167..a73019be 100644
--- a/metrics/metrics.go
+++ b/metrics/metrics.go
@@ -164,6 +164,7 @@ const PublisherUnknown = "unknown"
 const (
 	DemandWeb     DemandSource = "web"
 	DemandApp     DemandSource = "app"
+	DemandDOOH    DemandSource = "dooh"
 	DemandUnknown DemandSource = "unknown"
 )
 
@@ -171,22 +172,25 @@ func DemandTypes() []DemandSource {
 	return []DemandSource{
 		DemandWeb,
 		DemandApp,
+		DemandDOOH,
 		DemandUnknown,
 	}
 }
 
 // The request types (endpoints)
 const (
-	ReqTypeORTB2Web RequestType = "openrtb2-web"
-	ReqTypeORTB2App RequestType = "openrtb2-app"
-	ReqTypeAMP      RequestType = "amp"
-	ReqTypeVideo    RequestType = "video"
+	ReqTypeORTB2Web  RequestType = "openrtb2-web"
+	ReqTypeORTB2App  RequestType = "openrtb2-app"
+	ReqTypeORTB2DOOH RequestType = "openrtb2-dooh"
+	ReqTypeAMP       RequestType = "amp"
+	ReqTypeVideo     RequestType = "video"
 )
 
 func RequestTypes() []RequestType {
 	return []RequestType{
 		ReqTypeORTB2Web,
 		ReqTypeORTB2App,
+		ReqTypeORTB2DOOH,
 		ReqTypeAMP,
 		ReqTypeVideo,
 	}
@@ -267,6 +271,7 @@ const (
 	AdapterErrorTimeout             AdapterError = "timeout"
 	AdapterErrorFailedToRequestBids AdapterError = "failedtorequestbid"
 	AdapterErrorValidation          AdapterError = "validation"
+	AdapterErrorTmaxTimeout         AdapterError = "tmaxtimeout"
 	AdapterErrorUnknown             AdapterError = "unknown_error"
 )
 
diff --git a/metrics/prometheus/preload.go b/metrics/prometheus/preload.go
index dae7c14d..59f70cfb 100644
--- a/metrics/prometheus/preload.go
+++ b/metrics/prometheus/preload.go
@@ -2,28 +2,30 @@ package prometheusmetrics
 
 import (
 	"github.com/prebid/prebid-server/metrics"
+	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/prometheus/client_golang/prometheus"
 )
 
 func preloadLabelValues(m *Metrics, syncerKeys []string, moduleStageNames map[string][]string) {
 	var (
-		setUidStatusValues        = setUidStatusesAsString()
-		adapterErrorValues        = adapterErrorsAsString()
-		adapterValues             = adaptersAsString()
+		adapterErrorValues        = enumAsString(metrics.AdapterErrors())
+		adapterValues             = enumAsString(openrtb_ext.CoreBidderNames())
 		bidTypeValues             = []string{markupDeliveryAdm, markupDeliveryNurl}
 		boolValues                = boolValuesAsString()
-		cacheResultValues         = cacheResultsAsString()
+		cacheResultValues         = enumAsString(metrics.CacheResults())
 		connectionErrorValues     = []string{connectionAcceptError, connectionCloseError}
-		cookieValues              = cookieTypesAsString()
-		cookieSyncStatusValues    = cookieSyncStatusesAsString()
-		overheadTypes             = overheadTypesAsString()
-		requestTypeValues         = requestTypesAsString()
-		requestStatusValues       = requestStatusesAsString()
-		storedDataFetchTypeValues = storedDataFetchTypesAsString()
-		storedDataErrorValues     = storedDataErrorsAsString()
-		syncerRequestStatusValues = syncerRequestStatusesAsString()
-		syncerSetsStatusValues    = syncerSetStatusesAsString()
+		cookieSyncStatusValues    = enumAsString(metrics.CookieSyncStatuses())
+		cookieValues              = enumAsString(metrics.CookieTypes())
+		overheadTypes             = enumAsString(metrics.OverheadTypes())
+		requestStatusValues       = enumAsString(metrics.RequestStatuses())
+		requestTypeValues         = enumAsString(metrics.RequestTypes())
+		setUidStatusValues        = enumAsString(metrics.SetUidStatuses())
 		sourceValues              = []string{sourceRequest}
+		storedDataErrorValues     = enumAsString(metrics.StoredDataErrors())
+		storedDataFetchTypeValues = enumAsString(metrics.StoredDataFetchTypes())
+		syncerRequestStatusValues = enumAsString(metrics.SyncerRequestStatuses())
+		syncerSetsStatusValues    = enumAsString(metrics.SyncerSetUidStatuses())
+		tcfVersionValues          = enumAsString(metrics.TCFVersions())
 	)
 
 	preloadLabelValuesForCounter(m.connectionsError, map[string][]string{
@@ -224,7 +226,7 @@ func preloadLabelValues(m *Metrics, syncerKeys []string, moduleStageNames map[st
 
 	preloadLabelValuesForCounter(m.privacyTCF, map[string][]string{
 		sourceLabel:  sourceValues,
-		versionLabel: tcfVersionsAsString(),
+		versionLabel: tcfVersionValues,
 	})
 
 	if !m.metricsDisabled.AdapterGDPRRequestBlocked {
diff --git a/metrics/prometheus/type_conversion.go b/metrics/prometheus/type_conversion.go
index 07e4e89c..0ae5366b 100644
--- a/metrics/prometheus/type_conversion.go
+++ b/metrics/prometheus/type_conversion.go
@@ -2,22 +2,9 @@ package prometheusmetrics
 
 import (
 	"strconv"
-
-	"github.com/prebid/prebid-server/metrics"
-	"github.com/prebid/prebid-server/openrtb_ext"
 )
 
-func adaptersAsString() []string {
-	values := openrtb_ext.CoreBidderNames()
-	valuesAsString := make([]string, len(values))
-	for i, v := range values {
-		valuesAsString[i] = string(v)
-	}
-	return valuesAsString
-}
-
-func adapterErrorsAsString() []string {
-	values := metrics.AdapterErrors()
+func enumAsString[T ~string](values []T) []string {
 	valuesAsString := make([]string, len(values))
 	for i, v := range values {
 		valuesAsString[i] = string(v)
@@ -31,111 +18,3 @@ func boolValuesAsString() []string {
 		strconv.FormatBool(false),
 	}
 }
-
-func cacheResultsAsString() []string {
-	values := metrics.CacheResults()
-	valuesAsString := make([]string, len(values))
-	for i, v := range values {
-		valuesAsString[i] = string(v)
-	}
-	return valuesAsString
-}
-
-func cookieTypesAsString() []string {
-	values := metrics.CookieTypes()
-	valuesAsString := make([]string, len(values))
-	for i, v := range values {
-		valuesAsString[i] = string(v)
-	}
-	return valuesAsString
-}
-
-func cookieSyncStatusesAsString() []string {
-	values := metrics.CookieSyncStatuses()
-	valuesAsString := make([]string, len(values))
-	for i, v := range values {
-		valuesAsString[i] = string(v)
-	}
-	return valuesAsString
-}
-
-func requestStatusesAsString() []string {
-	values := metrics.RequestStatuses()
-	valuesAsString := make([]string, len(values))
-	for i, v := range values {
-		valuesAsString[i] = string(v)
-	}
-	return valuesAsString
-}
-
-func syncerRequestStatusesAsString() []string {
-	values := metrics.SyncerRequestStatuses()
-	valuesAsString := make([]string, len(values))
-	for i, v := range values {
-		valuesAsString[i] = string(v)
-	}
-	return valuesAsString
-}
-
-func overheadTypesAsString() []string {
-	overheadTypes := metrics.OverheadTypes()
-	overheadTypesAsString := make([]string, len(overheadTypes))
-	for i, ot := range overheadTypes {
-		overheadTypesAsString[i] = ot.String()
-	}
-	return overheadTypesAsString
-}
-
-func syncerSetStatusesAsString() []string {
-	values := metrics.SyncerSetUidStatuses()
-	valuesAsString := make([]string, len(values))
-	for i, v := range values {
-		valuesAsString[i] = string(v)
-	}
-	return valuesAsString
-}
-
-func requestTypesAsString() []string {
-	values := metrics.RequestTypes()
-	valuesAsString := make([]string, len(values))
-	for i, v := range values {
-		valuesAsString[i] = string(v)
-	}
-	return valuesAsString
-}
-
-func setUidStatusesAsString() []string {
-	values := metrics.SetUidStatuses()
-	valuesAsString := make([]string, len(values))
-	for i, v := range values {
-		valuesAsString[i] = string(v)
-	}
-	return valuesAsString
-}
-
-func storedDataFetchTypesAsString() []string {
-	values := metrics.StoredDataFetchTypes()
-	valuesAsString := make([]string, len(values))
-	for i, v := range values {
-		valuesAsString[i] = string(v)
-	}
-	return valuesAsString
-}
-
-func storedDataErrorsAsString() []string {
-	values := metrics.StoredDataErrors()
-	valuesAsString := make([]string, len(values))
-	for i, v := range values {
-		valuesAsString[i] = string(v)
-	}
-	return valuesAsString
-}
-
-func tcfVersionsAsString() []string {
-	values := metrics.TCFVersions()
-	valuesAsString := make([]string, len(values))
-	for i, v := range values {
-		valuesAsString[i] = string(v)
-	}
-	return valuesAsString
-}
diff --git a/openrtb_ext/request_wrapper.go b/openrtb_ext/request_wrapper.go
index 5f5636d6..f4ef69b0 100644
--- a/openrtb_ext/request_wrapper.go
+++ b/openrtb_ext/request_wrapper.go
@@ -42,6 +42,7 @@ type RequestWrapper struct {
 	appExt              *AppExt
 	regExt              *RegExt
 	siteExt             *SiteExt
+	doohExt             *DOOHExt
 	sourceExt           *SourceExt
 }
 
@@ -160,6 +161,17 @@ func (rw *RequestWrapper) GetSiteExt() (*SiteExt, error) {
 	return rw.siteExt, rw.siteExt.unmarshal(rw.Site.Ext)
 }
 
+func (rw *RequestWrapper) GetDOOHExt() (*DOOHExt, error) {
+	if rw.doohExt != nil {
+		return rw.doohExt, nil
+	}
+	rw.doohExt = &DOOHExt{}
+	if rw.BidRequest == nil || rw.DOOH == nil || rw.DOOH.Ext == nil {
+		return rw.doohExt, rw.doohExt.unmarshal(json.RawMessage{})
+	}
+	return rw.doohExt, rw.doohExt.unmarshal(rw.DOOH.Ext)
+}
+
 func (rw *RequestWrapper) GetSourceExt() (*SourceExt, error) {
 	if rw.sourceExt != nil {
 		return rw.sourceExt, nil
@@ -197,6 +209,9 @@ func (rw *RequestWrapper) RebuildRequest() error {
 	if err := rw.rebuildSiteExt(); err != nil {
 		return err
 	}
+	if err := rw.rebuildDOOHExt(); err != nil {
+		return err
+	}
 	if err := rw.rebuildSourceExt(); err != nil {
 		return err
 	}
@@ -297,6 +312,25 @@ func (rw *RequestWrapper) rebuildAppExt() error {
 	return nil
 }
 
+func (rw *RequestWrapper) rebuildDOOHExt() error {
+	if rw.doohExt == nil || !rw.doohExt.Dirty() {
+		return nil
+	}
+
+	doohJson, err := rw.doohExt.marshal()
+	if err != nil {
+		return err
+	}
+
+	if doohJson != nil && rw.DOOH == nil {
+		rw.DOOH = &openrtb2.DOOH{Ext: doohJson}
+	} else if rw.DOOH != nil {
+		rw.DOOH.Ext = doohJson
+	}
+
+	return nil
+}
+
 func (rw *RequestWrapper) rebuildRegExt() error {
 	if rw.regExt == nil || !rw.regExt.Dirty() {
 		return nil
@@ -370,6 +404,7 @@ func (rw *RequestWrapper) Clone() *RequestWrapper {
 	clone.appExt = rw.appExt.Clone()
 	clone.regExt = rw.regExt.Clone()
 	clone.siteExt = rw.siteExt.Clone()
+	clone.doohExt = rw.doohExt.Clone()
 	clone.sourceExt = rw.sourceExt.Clone()
 
 	return &clone
@@ -1040,6 +1075,70 @@ func (ae *AppExt) Clone() *AppExt {
 	return &clone
 }
 
+// ---------------------------------------------------------------
+// DOOHExt provides an interface for request.dooh.ext
+// This is currently a placeholder for consistency with others - no useful attributes and getters/setters exist yet
+// ---------------------------------------------------------------
+
+type DOOHExt struct {
+	ext      map[string]json.RawMessage
+	extDirty bool
+}
+
+func (de *DOOHExt) unmarshal(extJson json.RawMessage) error {
+	if len(de.ext) != 0 || de.Dirty() {
+		return nil
+	}
+
+	de.ext = make(map[string]json.RawMessage)
+
+	if len(extJson) == 0 {
+		return nil
+	}
+
+	if err := json.Unmarshal(extJson, &de.ext); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (de *DOOHExt) marshal() (json.RawMessage, error) {
+	de.extDirty = false
+	if len(de.ext) == 0 {
+		return nil, nil
+	}
+	return json.Marshal(de.ext)
+}
+
+func (de *DOOHExt) Dirty() bool {
+	return de.extDirty
+}
+
+func (de *DOOHExt) GetExt() map[string]json.RawMessage {
+	ext := make(map[string]json.RawMessage)
+	for k, v := range de.ext {
+		ext[k] = v
+	}
+	return ext
+}
+
+func (de *DOOHExt) SetExt(ext map[string]json.RawMessage) {
+	de.ext = ext
+	de.extDirty = true
+}
+
+func (de *DOOHExt) Clone() *DOOHExt {
+	if de == nil {
+		return nil
+	}
+
+	clone := *de
+	clone.ext = maputil.Clone(de.ext)
+
+	return &clone
+}
+
 // ---------------------------------------------------------------
 // RegExt provides an interface for request.regs.ext
 // ---------------------------------------------------------------


Test patch:
diff --git a/adapters/infoawarebidder_test.go b/adapters/infoawarebidder_test.go
index f42716bc..87d9d4d4 100644
--- a/adapters/infoawarebidder_test.go
+++ b/adapters/infoawarebidder_test.go
@@ -10,6 +10,7 @@ import (
 	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestAppNotSupported(t *testing.T) {
@@ -56,6 +57,26 @@ func TestSiteNotSupported(t *testing.T) {
 	assert.Len(t, bids, 0)
 }
 
+func TestDOOHNotSupported(t *testing.T) {
+	bidder := &mockBidder{}
+	info := config.BidderInfo{
+		Capabilities: &config.CapabilitiesInfo{
+			Site: &config.PlatformInfo{
+				MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner},
+			},
+		},
+	}
+	constrained := adapters.BuildInfoAwareBidder(bidder, info)
+	bids, errs := constrained.MakeRequests(&openrtb2.BidRequest{
+		Imp:  []openrtb2.Imp{{ID: "imp-1", Banner: &openrtb2.Banner{}}},
+		DOOH: &openrtb2.DOOH{},
+	}, &adapters.ExtraRequestInfo{})
+	require.Len(t, errs, 1)
+	assert.EqualError(t, errs[0], "this bidder does not support dooh requests")
+	assert.IsType(t, &errortypes.Warning{}, errs[0])
+	assert.Len(t, bids, 0)
+}
+
 func TestImpFiltering(t *testing.T) {
 	bidder := &mockBidder{}
 	info := config.BidderInfo{
@@ -66,6 +87,9 @@ func TestImpFiltering(t *testing.T) {
 			App: &config.PlatformInfo{
 				MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner},
 			},
+			DOOH: &config.PlatformInfo{
+				MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeNative},
+			},
 		},
 	}
 
@@ -153,10 +177,10 @@ func TestImpFiltering(t *testing.T) {
 			description: "All imps with correct media type, MakeRequest() call expected",
 			inBidRequest: &openrtb2.BidRequest{
 				Imp: []openrtb2.Imp{
-					{ID: "imp-1", Video: &openrtb2.Video{}},
-					{ID: "imp-2", Video: &openrtb2.Video{}},
+					{ID: "imp-1", Native: &openrtb2.Native{}},
+					{ID: "imp-2", Native: &openrtb2.Native{}},
 				},
-				Site: &openrtb2.Site{},
+				DOOH: &openrtb2.DOOH{},
 			},
 			expectedErrors: nil,
 			expectedImpLen: 2,
diff --git a/config/account_test.go b/config/account_test.go
index 31d9796a..58f2a52e 100644
--- a/config/account_test.go
+++ b/config/account_test.go
@@ -205,6 +205,7 @@ func TestAccountChannelGetByChannelType(t *testing.T) {
 		giveAppEnabled   *bool
 		giveVideoEnabled *bool
 		giveWebEnabled   *bool
+		giveDOOHEnabled  *bool
 		giveChannelType  ChannelType
 		wantEnabled      *bool
 	}{
@@ -276,6 +277,23 @@ func TestAccountChannelGetByChannelType(t *testing.T) {
 			giveChannelType: ChannelWeb,
 			wantEnabled:     &trueValue,
 		},
+		{
+			description:     "DOOH channel setting unspecified, returns nil",
+			giveChannelType: ChannelDOOH,
+			wantEnabled:     nil,
+		},
+		{
+			description:     "DOOH channel disabled, returns false",
+			giveDOOHEnabled: &falseValue,
+			giveChannelType: ChannelDOOH,
+			wantEnabled:     &falseValue,
+		},
+		{
+			description:     "DOOH channel enabled, returns true",
+			giveDOOHEnabled: &trueValue,
+			giveChannelType: ChannelDOOH,
+			wantEnabled:     &trueValue,
+		},
 	}
 
 	for _, tt := range tests {
@@ -284,6 +302,7 @@ func TestAccountChannelGetByChannelType(t *testing.T) {
 			App:   tt.giveAppEnabled,
 			Video: tt.giveVideoEnabled,
 			Web:   tt.giveWebEnabled,
+			DOOH:  tt.giveDOOHEnabled,
 		}
 
 		result := accountChannel.GetByChannelType(tt.giveChannelType)
@@ -836,7 +855,7 @@ func TestAccountChannelIsSet(t *testing.T) {
 	}{
 		{
 			name:                "AccountChannelSetAllFields",
-			givenAccountChannel: &AccountChannel{AMP: &trueBool, App: &falseBool, Video: &falseBool, Web: &falseBool},
+			givenAccountChannel: &AccountChannel{AMP: &trueBool, App: &falseBool, Video: &falseBool, Web: &falseBool, DOOH: &falseBool},
 			expected:            true,
 		},
 		{
diff --git a/config/bidderinfo_test.go b/config/bidderinfo_test.go
index 77625c0c..438ed5af 100644
--- a/config/bidderinfo_test.go
+++ b/config/bidderinfo_test.go
@@ -31,6 +31,9 @@ capabilities:
       - banner
       - video
       - native
+  dooh:
+    mediaTypes:
+      - banner
 modifyingVastXmlAllowed: true
 debug:
   allow: true
@@ -92,6 +95,9 @@ func TestLoadBidderInfoFromDisk(t *testing.T) {
 				Site: &PlatformInfo{
 					MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner, openrtb_ext.BidTypeVideo, openrtb_ext.BidTypeNative},
 				},
+				DOOH: &PlatformInfo{
+					MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner, openrtb_ext.BidTypeVideo},
+				},
 			},
 			Syncer: &Syncer{
 				Key: "foo",
@@ -176,6 +182,9 @@ func TestProcessBidderInfo(t *testing.T) {
 						Site: &PlatformInfo{
 							MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner, openrtb_ext.BidTypeVideo, openrtb_ext.BidTypeNative},
 						},
+						DOOH: &PlatformInfo{
+							MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner},
+						},
 					},
 					Debug: &DebugInfo{
 						Allow: true,
@@ -225,6 +234,9 @@ func TestProcessBidderInfo(t *testing.T) {
 						Site: &PlatformInfo{
 							MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner, openrtb_ext.BidTypeVideo, openrtb_ext.BidTypeNative},
 						},
+						DOOH: &PlatformInfo{
+							MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner},
+						},
 					},
 					Debug: &DebugInfo{
 						Allow: true,
@@ -533,7 +545,7 @@ func TestBidderInfoValidationPositive(t *testing.T) {
 			Endpoint:   "http://bidderB.com/openrtb2",
 			PlatformID: "B",
 			Maintainer: &MaintainerInfo{
-				Email: "maintainer@bidderA.com",
+				Email: "maintainer@bidderB.com",
 			},
 			GVLVendorID: 2,
 			Capabilities: &CapabilitiesInfo{
@@ -569,6 +581,23 @@ func TestBidderInfoValidationPositive(t *testing.T) {
 			},
 			AliasOf: "bidderB",
 		},
+		"bidderD": BidderInfo{
+			Endpoint:   "http://bidderD.com/openrtb2",
+			PlatformID: "D",
+			Maintainer: &MaintainerInfo{
+				Email: "maintainer@bidderD.com",
+			},
+			GVLVendorID: 3,
+			Capabilities: &CapabilitiesInfo{
+				DOOH: &PlatformInfo{
+					MediaTypes: []openrtb_ext.BidType{
+						openrtb_ext.BidTypeVideo,
+						openrtb_ext.BidTypeNative,
+						openrtb_ext.BidTypeBanner,
+					},
+				},
+			},
+		},
 	}
 	errs := bidderInfos.validate(make([]error, 0))
 	assert.Len(t, errs, 0, "All bidder infos should be correct")
@@ -769,7 +798,7 @@ func TestBidderInfoValidationNegative(t *testing.T) {
 			},
 		},
 		{
-			"One bidder missing capabilities site and app",
+			"One bidder missing capabilities site and app and dooh",
 			BidderInfos{
 				"bidderA": BidderInfo{
 					Endpoint: "http://bidderA.com/openrtb2",
@@ -780,7 +809,7 @@ func TestBidderInfoValidationNegative(t *testing.T) {
 				},
 			},
 			[]error{
-				errors.New("at least one of capabilities.site or capabilities.app must exist for adapter: bidderA"),
+				errors.New("at least one of capabilities.site, capabilities.app, or capabilities.dooh must exist for adapter: bidderA"),
 			},
 		},
 		{
@@ -804,6 +833,27 @@ func TestBidderInfoValidationNegative(t *testing.T) {
 				errors.New("capabilities.app failed validation: unrecognized media type at index 0: incorrect for adapter: bidderA"),
 			},
 		},
+		{
+			"One bidder incorrect capabilities for dooh",
+			BidderInfos{
+				"bidderA": BidderInfo{
+					Endpoint: "http://bidderA.com/openrtb2",
+					Maintainer: &MaintainerInfo{
+						Email: "maintainer@bidderA.com",
+					},
+					Capabilities: &CapabilitiesInfo{
+						DOOH: &PlatformInfo{
+							MediaTypes: []openrtb_ext.BidType{
+								"incorrect",
+							},
+						},
+					},
+				},
+			},
+			[]error{
+				errors.New("capabilities.dooh failed validation: unrecognized media type at index 0: incorrect for adapter: bidderA"),
+			},
+		},
 		{
 			"One bidder nil capabilities",
 			BidderInfos{
@@ -1008,7 +1058,7 @@ func TestBidderInfoValidationNegative(t *testing.T) {
 				},
 			},
 			[]error{
-				errors.New("at least one of capabilities.site or capabilities.app must exist for adapter: bidderA"),
+				errors.New("at least one of capabilities.site, capabilities.app, or capabilities.dooh must exist for adapter: bidderA"),
 				errors.New("capabilities for alias: bidderB should be a subset of capabilities for parent bidder: bidderA"),
 			},
 		},
@@ -1687,6 +1737,9 @@ func TestReadFullYamlBidderConfig(t *testing.T) {
 				Site: &PlatformInfo{
 					MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner, openrtb_ext.BidTypeVideo, openrtb_ext.BidTypeNative},
 				},
+				DOOH: &PlatformInfo{
+					MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner},
+				},
 			},
 			ModifyingVastXmlAllowed: true,
 			Debug: &DebugInfo{
diff --git a/config/test/bidder-info-valid/stroeerCore.yaml b/config/test/bidder-info-valid/stroeerCore.yaml
index 0a1b3059..ef95d238 100644
--- a/config/test/bidder-info-valid/stroeerCore.yaml
+++ b/config/test/bidder-info-valid/stroeerCore.yaml
@@ -11,6 +11,10 @@ capabilities:
       - banner
       - video
       - native
+  dooh:
+    mediaTypes:
+      - banner
+      - video
 userSync:
   key: "foo"
   default: "iframe"
diff --git a/endpoints/info/bidders_detail_test.go b/endpoints/info/bidders_detail_test.go
index 47e5a068..3be24acf 100644
--- a/endpoints/info/bidders_detail_test.go
+++ b/endpoints/info/bidders_detail_test.go
@@ -225,6 +225,7 @@ func TestMapDetailFromConfig(t *testing.T) {
 				Capabilities: &config.CapabilitiesInfo{
 					App:  &config.PlatformInfo{MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner}},
 					Site: &config.PlatformInfo{MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeVideo}},
+					DOOH: &config.PlatformInfo{MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeNative}},
 				},
 			},
 			expected: bidderDetail{
@@ -236,6 +237,7 @@ func TestMapDetailFromConfig(t *testing.T) {
 				Capabilities: &capabilities{
 					App:  &platform{MediaTypes: []string{"banner"}},
 					Site: &platform{MediaTypes: []string{"video"}},
+					DOOH: &platform{MediaTypes: []string{"native"}},
 				},
 				AliasOf: "",
 			},
diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go
index 84e5cf4b..08637df2 100644
--- a/endpoints/openrtb2/auction_test.go
+++ b/endpoints/openrtb2/auction_test.go
@@ -205,7 +205,7 @@ func runEndToEndTest(t *testing.T, auctionEndpointHandler httprouter.Handle, tes
 		err = json.Unmarshal(test.ExpectedBidResponse, &expectedBidResponse)
 		if assert.NoError(t, err, "Could not unmarshal expected bidResponse taken from test file.\n Test file: %s\n Error:%s\n", testFile, err) {
 			err = json.Unmarshal([]byte(actualJsonBidResponse), &actualBidResponse)
-			if assert.NoError(t, err, "Could not unmarshal actual bidResponse from auction.\n Test file: %s\n Error:%s\n", testFile, err) {
+			if assert.NoError(t, err, "Could not unmarshal actual bidResponse from auction.\n Test file: %s\n Error:%s\n actualJsonBidResponse: %s", testFile, err, actualJsonBidResponse) {
 				assertBidResponseEqual(t, testFile, expectedBidResponse, actualBidResponse)
 			}
 		}
@@ -1541,6 +1541,81 @@ func TestMergeBidderParamsImpExtPrebid(t *testing.T) {
 	}
 }
 
+func TestValidateExactlyOneInventoryType(t *testing.T) {
+
+	testCases := []struct {
+		description         string
+		givenRequestWrapper *openrtb_ext.RequestWrapper
+		expectedError       error
+	}{
+		{
+			description:         "None provided - invalid",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}},
+			expectedError:       errors.New("One of request.site or request.app or request.dooh must be defined"),
+		},
+		{
+			description: "Only site provided",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{
+				Site: &openrtb2.Site{},
+			}},
+			expectedError: nil,
+		},
+		{
+			description: "Only app provided",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{
+				App: &openrtb2.App{},
+			}},
+			expectedError: nil,
+		},
+		{
+			description: "Only dooh provided",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{
+				DOOH: &openrtb2.DOOH{},
+			}},
+			expectedError: nil,
+		},
+		{
+			description: "Two provided (site+app) - invalid",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{
+				Site: &openrtb2.Site{},
+				App:  &openrtb2.App{},
+			}},
+			expectedError: errors.New("No more than one of request.site or request.app or request.dooh can be defined"),
+		},
+		{
+			description: "Two provided (site+dooh) - invalid",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{
+				Site: &openrtb2.Site{},
+				DOOH: &openrtb2.DOOH{},
+			}},
+			expectedError: errors.New("No more than one of request.site or request.app or request.dooh can be defined"),
+		},
+		{
+			description: "Two provided (app+dooh) - invalid",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{
+				App:  &openrtb2.App{},
+				DOOH: &openrtb2.DOOH{},
+			}},
+			expectedError: errors.New("No more than one of request.site or request.app or request.dooh can be defined"),
+		},
+		{
+			description: "Three provided - invalid",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{
+				Site: &openrtb2.Site{},
+				App:  &openrtb2.App{},
+				DOOH: &openrtb2.DOOH{},
+			}},
+			expectedError: errors.New("No more than one of request.site or request.app or request.dooh can be defined"),
+		},
+	}
+
+	for _, test := range testCases {
+		error := validateExactlyOneInventoryType(test.givenRequestWrapper)
+		assert.Equalf(t, test.expectedError, error, "Error doesn't match: %s\n", test.description)
+	}
+
+}
+
 func TestValidateRequest(t *testing.T) {
 	deps := &endpointDeps{
 		fakeUUIDGenerator{},
@@ -1725,6 +1800,66 @@ func TestValidateRequest(t *testing.T) {
 			expectedErrorList:     []error{},
 			expectedChannelObject: &openrtb_ext.ExtRequestPrebidChannel{Name: appChannel, Version: ""},
 		},
+		{
+			description: "Minimum required site attributes missing",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					ID:   "Some-ID",
+					Site: &openrtb2.Site{},
+					Imp: []openrtb2.Imp{
+						{
+							ID: "Some-Imp-ID",
+							Banner: &openrtb2.Banner{
+								Format: []openrtb2.Format{
+									{
+										W: 600,
+										H: 500,
+									},
+									{
+										W: 300,
+										H: 600,
+									},
+								},
+							},
+							Ext: []byte(`{"appnexus":{"placementId": 12345678}}`),
+						},
+					},
+				},
+			},
+			expectedErrorList: []error{
+				errors.New("request.site should include at least one of request.site.id or request.site.page."),
+			},
+		},
+		{
+			description: "Minimum required DOOH attributes missing",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					ID:   "Some-ID",
+					DOOH: &openrtb2.DOOH{},
+					Imp: []openrtb2.Imp{
+						{
+							ID: "Some-Imp-ID",
+							Banner: &openrtb2.Banner{
+								Format: []openrtb2.Format{
+									{
+										W: 600,
+										H: 500,
+									},
+									{
+										W: 300,
+										H: 600,
+									},
+								},
+							},
+							Ext: []byte(`{"appnexus":{"placementId": 12345678}}`),
+						},
+					},
+				},
+			},
+			expectedErrorList: []error{
+				errors.New("request.dooh should include at least one of request.dooh.id or request.dooh.venuetype."),
+			},
+		},
 	}
 
 	for _, test := range testCases {
@@ -2833,19 +2968,21 @@ func TestValidateImpExt(t *testing.T) {
 
 	for _, group := range testGroups {
 		for _, test := range group.testCases {
-			imp := &openrtb2.Imp{Ext: test.impExt}
-			impWrapper := &openrtb_ext.ImpWrapper{Imp: imp}
+			t.Run(test.description, func(t *testing.T) {
+				imp := &openrtb2.Imp{Ext: test.impExt}
+				impWrapper := &openrtb_ext.ImpWrapper{Imp: imp}
 
-			errs := deps.validateImpExt(impWrapper, nil, 0, false, nil)
+				errs := deps.validateImpExt(impWrapper, nil, 0, false, nil)
 
-			assert.NoError(t, impWrapper.RebuildImp(), test.description+":rebuild_imp")
+				assert.NoError(t, impWrapper.RebuildImp(), test.description+":rebuild_imp")
 
-			if len(test.expectedImpExt) > 0 {
-				assert.JSONEq(t, test.expectedImpExt, string(imp.Ext), "imp.ext JSON does not match expected. Test: %s. %s\n", group.description, test.description)
-			} else {
-				assert.Empty(t, imp.Ext, "imp.ext expected to be empty but was: %s. Test: %s. %s\n", string(imp.Ext), group.description, test.description)
-			}
-			assert.Equal(t, test.expectedErrs, errs, "errs slice does not match expected. Test: %s. %s\n", group.description, test.description)
+				if len(test.expectedImpExt) > 0 {
+					assert.JSONEq(t, test.expectedImpExt, string(imp.Ext), "imp.ext JSON does not match expected. Test: %s. %s\n", group.description, test.description)
+				} else {
+					assert.Empty(t, imp.Ext, "imp.ext expected to be empty but was: %s. Test: %s. %s\n", string(imp.Ext), group.description, test.description)
+				}
+				assert.Equal(t, test.expectedErrs, errs, "errs slice does not match expected. Test: %s. %s\n", group.description, test.description)
+			})
 		}
 	}
 }
@@ -3237,6 +3374,78 @@ func TestMapSChains(t *testing.T) {
 	}
 }
 
+func TestSearchAccountID(t *testing.T) {
+	// Correctness for lookup within Publisher object left to TestGetAccountID
+	// This however tests the expected lookup paths in outer site, app and dooh
+	testCases := []struct {
+		description       string
+		request           []byte
+		expectedAccID     string
+		expectedError     error
+		expectedIsAppReq  bool
+		expectedIsSiteReq bool
+		expectedIsDOOHReq bool
+	}{
+		{
+			description:       "No publisher available",
+			request:           []byte(`{}`),
+			expectedAccID:     "",
+			expectedError:     nil,
+			expectedIsAppReq:  false,
+			expectedIsDOOHReq: false,
+		},
+		{
+			description:       "Publisher.ID doesn't exist",
+			request:           []byte(`{"site":{"publisher":{}}}`),
+			expectedAccID:     "",
+			expectedError:     nil,
+			expectedIsAppReq:  false,
+			expectedIsDOOHReq: false,
+		},
+		{
+			description:       "Publisher.ID not a string",
+			request:           []byte(`{"site":{"publisher":{"id":42}}}`),
+			expectedAccID:     "",
+			expectedError:     errors.New("site.publisher.id must be a string"),
+			expectedIsAppReq:  false,
+			expectedIsDOOHReq: false,
+		},
+		{
+			description:       "Publisher available in request.site",
+			request:           []byte(`{"site":{"publisher":{"id":"42"}}}`),
+			expectedAccID:     "42",
+			expectedError:     nil,
+			expectedIsAppReq:  false,
+			expectedIsDOOHReq: false,
+		},
+		{
+			description:       "Publisher available in request.app",
+			request:           []byte(`{"app":{"publisher":{"id":"42"}}}`),
+			expectedAccID:     "42",
+			expectedError:     nil,
+			expectedIsAppReq:  true,
+			expectedIsDOOHReq: false,
+		},
+		{
+			description:       "Publisher available in request.dooh",
+			request:           []byte(`{"dooh":{"publisher":{"id":"42"}}}`),
+			expectedAccID:     "42",
+			expectedError:     nil,
+			expectedIsAppReq:  false,
+			expectedIsDOOHReq: true,
+		},
+	}
+
+	for _, test := range testCases {
+		accountId, isAppReq, isDOOHReq, err := searchAccountId(test.request)
+		assert.Equal(t, test.expectedAccID, accountId, "searchAccountID should return expected account ID for test case: %s", test.description)
+		assert.Equal(t, test.expectedIsAppReq, isAppReq, "searchAccountID should return expected isAppReq for test case: %s", test.description)
+		assert.Equal(t, test.expectedIsDOOHReq, isDOOHReq, "searchAccountID should return expected isDOOHReq for test case: %s", test.description)
+		assert.Equal(t, test.expectedError, err, "searchAccountID should return expected error for test case: %s", test.description)
+	}
+
+}
+
 func TestGetAccountID(t *testing.T) {
 	testPubID := "test-pub"
 	testParentAccount := "test-account"
@@ -5771,3 +5980,59 @@ func TestSetSeatNonBidRaw(t *testing.T) {
 		})
 	}
 }
+
+func TestValidateAliases(t *testing.T) {
+	deps := &endpointDeps{
+		disabledBidders: map[string]string{"rubicon": "rubicon"},
+		bidderMap:       map[string]openrtb_ext.BidderName{"appnexus": openrtb_ext.BidderName("appnexus")},
+	}
+
+	testCases := []struct {
+		description     string
+		aliases         map[string]string
+		expectedAliases map[string]string
+		expectedError   error
+	}{
+		{
+			description:     "valid case",
+			aliases:         map[string]string{"test": "appnexus"},
+			expectedAliases: map[string]string{"test": "appnexus"},
+			expectedError:   nil,
+		},
+		{
+			description:     "valid case - case insensitive",
+			aliases:         map[string]string{"test": "Appnexus"},
+			expectedAliases: map[string]string{"test": "appnexus"},
+			expectedError:   nil,
+		},
+		{
+			description:     "disabled bidder",
+			aliases:         map[string]string{"test": "rubicon"},
+			expectedAliases: nil,
+			expectedError:   errors.New("request.ext.prebid.aliases.test refers to disabled bidder: rubicon"),
+		},
+		{
+			description:     "coreBidderName not found",
+			aliases:         map[string]string{"test": "anyBidder"},
+			expectedAliases: nil,
+			expectedError:   errors.New("request.ext.prebid.aliases.test refers to unknown bidder: anyBidder"),
+		},
+		{
+			description:     "alias name is coreBidder name",
+			aliases:         map[string]string{"appnexus": "appnexus"},
+			expectedAliases: nil,
+			expectedError:   errors.New("request.ext.prebid.aliases.appnexus defines a no-op alias. Choose a different alias, or remove this entry."),
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.description, func(t *testing.T) {
+			err := deps.validateAliases(testCase.aliases)
+			if err != nil {
+				assert.Equal(t, testCase.expectedError, err)
+			} else {
+				assert.ObjectsAreEqualValues(testCase.expectedAliases, map[string]string{"test": "appnexus"})
+			}
+		})
+	}
+}
diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go
index e2d476c0..65f6b795 100644
--- a/exchange/bidder_test.go
+++ b/exchange/bidder_test.go
@@ -3221,7 +3221,7 @@ func TestDoRequestImplWithTmax(t *testing.T) {
 			ctxDeadline:     time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
 			description:     "returns-tmax-timeout-error",
 			tmaxAdjustments: &TmaxAdjustmentsPreprocessed{IsEnforced: true, PBSResponsePreparationDuration: 100, BidderNetworkLatencyBuffer: 10, BidderResponseDurationMin: 5000},
-			assertFn:        func(err error) { assert.Equal(t, errTmaxTimeout, err) },
+			assertFn:        func(err error) { assert.Equal(t, &errortypes.TmaxTimeout{Message: "exceeded tmax duration"}, err) },
 		},
 		{
 			ctxDeadline:     time.Now().Add(5 * time.Second),
@@ -3296,7 +3296,7 @@ func TestDoRequestImplWithTmaxTimeout(t *testing.T) {
 			ctxDeadline:     time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
 			description:     "returns-tmax-timeout-error",
 			tmaxAdjustments: &TmaxAdjustmentsPreprocessed{IsEnforced: true, PBSResponsePreparationDuration: 100, BidderNetworkLatencyBuffer: 10, BidderResponseDurationMin: 5000},
-			assertFn:        func(err error) { assert.Equal(t, errTmaxTimeout, err) },
+			assertFn:        func(err error) { assert.Equal(t, &errortypes.TmaxTimeout{Message: "exceeded tmax duration"}, err) },
 		},
 	}
 	for _, test := range tests {
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index 8ca96880..c4b85ebe 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -769,7 +769,7 @@ func TestAdapterCurrency(t *testing.T) {
 		categoriesFetcher: nilCategoryFetcher{},
 		bidIDGenerator:    &mockBidIDGenerator{false, false},
 		adapterMap: map[openrtb_ext.BidderName]AdaptedBidder{
-			openrtb_ext.BidderName("foo"): AdaptBidder(mockBidder, nil, &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderName("foo"), nil, ""),
+			openrtb_ext.BidderName("appnexus"): AdaptBidder(mockBidder, nil, &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderName("appnexus"), nil, ""),
 		},
 	}
 	e.requestSplitter = requestSplitter{
@@ -783,7 +783,7 @@ func TestAdapterCurrency(t *testing.T) {
 		Imp: []openrtb2.Imp{{
 			ID:     "some-impression-id",
 			Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}},
-			Ext:    json.RawMessage(`{"prebid":{"bidder":{"foo":{"placementId":1}}}}`),
+			Ext:    json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placementId":1}}}}`),
 		}},
 		Site: &openrtb2.Site{
 			Page: "prebid.org",
@@ -805,7 +805,7 @@ func TestAdapterCurrency(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Equal(t, "some-request-id", response.ID, "Response ID")
 	assert.Empty(t, response.SeatBid, "Response Bids")
-	assert.Contains(t, string(response.Ext), `"errors":{"foo":[{"code":5,"message":"The adapter failed to generate any bid requests, but also failed to generate an error explaining why"}]}`, "Response Ext")
+	assert.Contains(t, string(response.Ext), `"errors":{"appnexus":[{"code":5,"message":"The adapter failed to generate any bid requests, but also failed to generate an error explaining why"}]}`, "Response Ext")
 
 	// Test Currency Converter Properly Passed To Adapter
 	if assert.NotNil(t, mockBidder.lastExtraRequestInfo, "Currency Conversion Argument") {
diff --git a/openrtb_ext/request_wrapper_test.go b/openrtb_ext/request_wrapper_test.go
index e02dd741..afa047e6 100644
--- a/openrtb_ext/request_wrapper_test.go
+++ b/openrtb_ext/request_wrapper_test.go
@@ -41,6 +41,7 @@ func TestCloneRequestWrapper(t *testing.T) {
 				appExt:    &AppExt{prebidDirty: true},
 				regExt:    &RegExt{usPrivacy: "foo"},
 				siteExt:   &SiteExt{amp: ptrutil.ToPtr[int8](1)},
+				doohExt:   &DOOHExt{},
 				sourceExt: &SourceExt{schainDirty: true},
 			},
 			reqWrapCopy: &RequestWrapper{
@@ -60,6 +61,7 @@ func TestCloneRequestWrapper(t *testing.T) {
 				appExt:    &AppExt{prebidDirty: true},
 				regExt:    &RegExt{usPrivacy: "foo"},
 				siteExt:   &SiteExt{amp: ptrutil.ToPtr[int8](1)},
+				doohExt:   &DOOHExt{},
 				sourceExt: &SourceExt{schainDirty: true},
 			},
 			mutator: func(t *testing.T, reqWrap *RequestWrapper) {},
@@ -83,6 +85,7 @@ func TestCloneRequestWrapper(t *testing.T) {
 				appExt:    &AppExt{prebidDirty: true},
 				regExt:    &RegExt{usPrivacy: "foo"},
 				siteExt:   &SiteExt{amp: ptrutil.ToPtr[int8](1)},
+				doohExt:   &DOOHExt{},
 				sourceExt: &SourceExt{schainDirty: true},
 			},
 			reqWrapCopy: &RequestWrapper{
@@ -102,6 +105,7 @@ func TestCloneRequestWrapper(t *testing.T) {
 				appExt:    &AppExt{prebidDirty: true},
 				regExt:    &RegExt{usPrivacy: "foo"},
 				siteExt:   &SiteExt{amp: ptrutil.ToPtr[int8](1)},
+				doohExt:   &DOOHExt{},
 				sourceExt: &SourceExt{schainDirty: true},
 			},
 			mutator: func(t *testing.T, reqWrap *RequestWrapper) {
@@ -115,6 +119,7 @@ func TestCloneRequestWrapper(t *testing.T) {
 				reqWrap.appExt = nil
 				reqWrap.regExt = nil
 				reqWrap.siteExt = nil
+				reqWrap.doohExt = nil
 				reqWrap.sourceExt = nil
 			},
 		},
@@ -1206,6 +1211,141 @@ func TestCloneAppExt(t *testing.T) {
 	}
 }
 
+func TestRebuildDOOHExt(t *testing.T) {
+	// These permutations look a bit wonky
+	// Since DOOHExt currently exists for consistency but there isn't a single field
+	// expected - hence unable to test dirty and variations
+	// Once one is established, updated the permutations below similar to TestRebuildAppExt example
+	testCases := []struct {
+		description           string
+		request               openrtb2.BidRequest
+		requestDOOHExtWrapper DOOHExt
+		expectedRequest       openrtb2.BidRequest
+	}{
+		{
+			description:           "Nil - Not Dirty",
+			request:               openrtb2.BidRequest{},
+			requestDOOHExtWrapper: DOOHExt{},
+			expectedRequest:       openrtb2.BidRequest{},
+		},
+		{
+			description:           "Nil - Dirty",
+			request:               openrtb2.BidRequest{},
+			requestDOOHExtWrapper: DOOHExt{},
+			expectedRequest:       openrtb2.BidRequest{DOOH: nil},
+		},
+		{
+			description:           "Nil - Dirty - No Change",
+			request:               openrtb2.BidRequest{},
+			requestDOOHExtWrapper: DOOHExt{},
+			expectedRequest:       openrtb2.BidRequest{},
+		},
+		{
+			description:           "Empty - Not Dirty",
+			request:               openrtb2.BidRequest{DOOH: &openrtb2.DOOH{}},
+			requestDOOHExtWrapper: DOOHExt{},
+			expectedRequest:       openrtb2.BidRequest{DOOH: &openrtb2.DOOH{}},
+		},
+		{
+			description:           "Empty - Dirty",
+			request:               openrtb2.BidRequest{DOOH: &openrtb2.DOOH{}},
+			requestDOOHExtWrapper: DOOHExt{},
+			expectedRequest:       openrtb2.BidRequest{DOOH: &openrtb2.DOOH{}},
+		},
+		{
+			description:           "Empty - Dirty - No Change",
+			request:               openrtb2.BidRequest{DOOH: &openrtb2.DOOH{}},
+			requestDOOHExtWrapper: DOOHExt{},
+			expectedRequest:       openrtb2.BidRequest{DOOH: &openrtb2.DOOH{}},
+		},
+		{
+			description:           "Populated - Not Dirty",
+			request:               openrtb2.BidRequest{DOOH: &openrtb2.DOOH{Ext: json.RawMessage(`{}`)}},
+			requestDOOHExtWrapper: DOOHExt{},
+			expectedRequest:       openrtb2.BidRequest{DOOH: &openrtb2.DOOH{Ext: json.RawMessage(`{}`)}},
+		},
+		{
+			description:           "Populated - Dirty",
+			request:               openrtb2.BidRequest{DOOH: &openrtb2.DOOH{Ext: json.RawMessage(`{}`)}},
+			requestDOOHExtWrapper: DOOHExt{},
+			expectedRequest:       openrtb2.BidRequest{DOOH: &openrtb2.DOOH{Ext: json.RawMessage(`{}`)}},
+		},
+		{
+			description:           "Populated - Dirty - No Change",
+			request:               openrtb2.BidRequest{DOOH: &openrtb2.DOOH{Ext: json.RawMessage(`{}`)}},
+			requestDOOHExtWrapper: DOOHExt{},
+			expectedRequest:       openrtb2.BidRequest{DOOH: &openrtb2.DOOH{Ext: json.RawMessage(`{}`)}},
+		},
+		{
+			description:           "Populated - Dirty - Cleared",
+			request:               openrtb2.BidRequest{DOOH: &openrtb2.DOOH{Ext: json.RawMessage(`{}`)}},
+			requestDOOHExtWrapper: DOOHExt{},
+			expectedRequest:       openrtb2.BidRequest{DOOH: &openrtb2.DOOH{Ext: json.RawMessage(`{}`)}},
+		},
+	}
+
+	for _, test := range testCases {
+		// create required filed in the test loop to keep test declaration easier to read
+		test.requestDOOHExtWrapper.ext = make(map[string]json.RawMessage)
+
+		w := RequestWrapper{BidRequest: &test.request, doohExt: &test.requestDOOHExtWrapper}
+		w.RebuildRequest()
+		assert.Equal(t, test.expectedRequest, *w.BidRequest, test.description)
+	}
+}
+
+func TestCloneDOOHExt(t *testing.T) {
+	testCases := []struct {
+		name        string
+		DOOHExt     *DOOHExt
+		DOOHExtCopy *DOOHExt                             // manual copy of above ext object to verify against
+		mutator     func(t *testing.T, DOOHExt *DOOHExt) // function to modify the Ext object
+	}{
+		{
+			name:        "Nil", // Verify the nil case
+			DOOHExt:     nil,
+			DOOHExtCopy: nil,
+			mutator:     func(t *testing.T, DOOHExt *DOOHExt) {},
+		},
+		{
+			name: "NoMutate",
+			DOOHExt: &DOOHExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+			},
+			DOOHExtCopy: &DOOHExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+			},
+			mutator: func(t *testing.T, DOOHExt *DOOHExt) {},
+		},
+		{
+			name: "General",
+			DOOHExt: &DOOHExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+			},
+			DOOHExtCopy: &DOOHExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+			},
+			mutator: func(t *testing.T, DOOHExt *DOOHExt) {
+				DOOHExt.ext["A"] = json.RawMessage(`"string"`)
+				DOOHExt.ext["C"] = json.RawMessage(`{}`)
+				DOOHExt.extDirty = false
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			clone := test.DOOHExt.Clone()
+			test.mutator(t, test.DOOHExt)
+			assert.Equal(t, test.DOOHExtCopy, clone)
+		})
+	}
+}
+
 func TestCloneRegExt(t *testing.T) {
 	testCases := []struct {
 		name       string