Repository: prebid/prebid-server
Pull request #3720 (2024-06-04 04:45:51)

Imp FPD: Skip bidder params and native validation
This PR allows the imp validation logic used to both validate an incoming request and revalidate imps after merging FPD to be executed while skipping the expensive bidder params and native string validation.


Patch:
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index d6ee5303..57d9e73a 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -918,7 +918,7 @@ func (deps *endpointDeps) validateRequest(account *config.Account, httpReq *http
 		}
 		impIDs[imp.ID] = i
 
-		errs := deps.requestValidator.ValidateImp(imp, i, requestAliases, hasStoredAuctionResponses, storedBidResp)
+		errs := deps.requestValidator.ValidateImp(imp, ortb.ValidationConfig{}, i, requestAliases, hasStoredAuctionResponses, storedBidResp)
 		if len(errs) > 0 {
 			errL = append(errL, errs...)
 		}
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/invalid-bidder-params.json b/endpoints/openrtb2/sample-requests/invalid-whole/invalid-bidder-params.json
new file mode 100644
index 00000000..0107ef94
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/invalid-bidder-params.json
@@ -0,0 +1,38 @@
+{
+  "description": "Required appnexus bidder param is provided but the type is invalid",
+  "config": {
+    "realParamsValidator": true
+  },
+  "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": []
+          }
+        }
+      }
+    ],
+    "tmax": 500,
+    "ext": {}
+  },
+  "expectedReturnCode": 400,
+  "expectedErrorMessage": "Invalid request: request.imp[0].ext.prebid.bidder.appnexus failed validation.\nplacementId: Invalid type. Expected: [integer,string], given: array\n"
+}
\ No newline at end of file
diff --git a/exchange/utils.go b/exchange/utils.go
index 2074d7f2..9b4ffd21 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -9,8 +9,6 @@ import (
 	"math/rand"
 	"strings"
 
-	"github.com/prebid/prebid-server/v2/ortb"
-
 	"github.com/prebid/go-gdpr/vendorconsent"
 	gpplib "github.com/prebid/go-gpp"
 	gppConstants "github.com/prebid/go-gpp/constants"
@@ -22,7 +20,7 @@ import (
 	"github.com/prebid/prebid-server/v2/gdpr"
 	"github.com/prebid/prebid-server/v2/metrics"
 	"github.com/prebid/prebid-server/v2/openrtb_ext"
-	// "github.com/prebid/prebid-server/v2/ortb/validation"
+	"github.com/prebid/prebid-server/v2/ortb"
 	"github.com/prebid/prebid-server/v2/privacy"
 	"github.com/prebid/prebid-server/v2/privacy/ccpa"
 	"github.com/prebid/prebid-server/v2/privacy/lmt"
@@ -609,7 +607,11 @@ func splitImps(imps []openrtb2.Imp, requestValidator ortb.RequestValidator, requ
 					return nil, err
 				}
 				impWrapper := openrtb_ext.ImpWrapper{Imp: &impCopy}
-				if err := requestValidator.ValidateImp(&impWrapper, i, requestAliases, hasStoredAuctionResponses, storedBidResponses); err != nil {
+				cfg := ortb.ValidationConfig{
+					SkipBidderParams: true,
+					SkipNative:       true,
+				}
+				if err := requestValidator.ValidateImp(&impWrapper, cfg, i, requestAliases, hasStoredAuctionResponses, storedBidResponses); err != nil {
 					return nil, &errortypes.InvalidImpFirstPartyData{
 						Message: fmt.Sprintf("merging bidder imp first party data for imp %s results in an invalid imp: %v", imp.ID, err),
 					}
diff --git a/ortb/request_validator.go b/ortb/request_validator.go
index e38b8da2..efe047f9 100644
--- a/ortb/request_validator.go
+++ b/ortb/request_validator.go
@@ -9,8 +9,13 @@ import (
 	"github.com/prebid/prebid-server/v2/stored_responses"
 )
 
+type ValidationConfig struct {
+	SkipBidderParams bool
+	SkipNative       bool
+}
+
 type RequestValidator interface {
-	ValidateImp(imp *openrtb_ext.ImpWrapper, index int, aliases map[string]string, hasStoredAuctionResponses bool, storedBidResponses stored_responses.ImpBidderStoredResp) []error
+	ValidateImp(imp *openrtb_ext.ImpWrapper, cfg ValidationConfig, index int, aliases map[string]string, hasStoredAuctionResponses bool, storedBidResponses stored_responses.ImpBidderStoredResp) []error
 }
 
 func NewRequestValidator(bidderMap map[string]openrtb_ext.BidderName, disabledBidders map[string]string, paramsValidator openrtb_ext.BidderParamValidator) RequestValidator {
@@ -27,7 +32,7 @@ type standardRequestValidator struct {
 	paramsValidator openrtb_ext.BidderParamValidator
 }
 
-func (srv *standardRequestValidator) ValidateImp(imp *openrtb_ext.ImpWrapper, index int, aliases map[string]string, hasStoredAuctionResponses bool, storedBidResponses stored_responses.ImpBidderStoredResp) []error {
+func (srv *standardRequestValidator) ValidateImp(imp *openrtb_ext.ImpWrapper, cfg ValidationConfig, index int, aliases map[string]string, hasStoredAuctionResponses bool, storedBidResponses stored_responses.ImpBidderStoredResp) []error {
 	if imp.ID == "" {
 		return []error{fmt.Errorf("request.imp[%d] missing required field: \"id\"", index)}
 	}
@@ -52,15 +57,17 @@ func (srv *standardRequestValidator) ValidateImp(imp *openrtb_ext.ImpWrapper, in
 		return []error{err}
 	}
 
-	if err := fillAndValidateNative(imp.Native, index); err != nil {
-		return []error{err}
+	if !cfg.SkipNative {
+		if err := fillAndValidateNative(imp.Native, index); err != nil {
+			return []error{err}
+		}
 	}
 
 	if err := validatePmp(imp.PMP, index); err != nil {
 		return []error{err}
 	}
 
-	errL := srv.validateImpExt(imp, aliases, index, hasStoredAuctionResponses, storedBidResponses)
+	errL := srv.validateImpExt(imp, cfg, aliases, index, hasStoredAuctionResponses, storedBidResponses)
 	if len(errL) != 0 {
 		return errL
 	}
@@ -68,7 +75,7 @@ func (srv *standardRequestValidator) ValidateImp(imp *openrtb_ext.ImpWrapper, in
 	return nil
 }
 
-func (srv *standardRequestValidator) validateImpExt(imp *openrtb_ext.ImpWrapper, aliases map[string]string, impIndex int, hasStoredAuctionResponses bool, storedBidResp stored_responses.ImpBidderStoredResp) []error {
+func (srv *standardRequestValidator) validateImpExt(imp *openrtb_ext.ImpWrapper, cfg ValidationConfig, aliases map[string]string, impIndex int, hasStoredAuctionResponses bool, storedBidResp stored_responses.ImpBidderStoredResp) []error {
 	if len(imp.Ext) == 0 {
 		return []error{fmt.Errorf("request.imp[%d].ext is required", impIndex)}
 	}
@@ -117,8 +124,10 @@ func (srv *standardRequestValidator) validateImpExt(imp *openrtb_ext.ImpWrapper,
 		}
 
 		if coreBidderNormalized, isValid := srv.bidderMap[coreBidder.String()]; isValid {
-			if err := srv.paramsValidator.Validate(coreBidderNormalized, ext); err != nil {
-				return []error{fmt.Errorf("request.imp[%d].ext.prebid.bidder.%s failed validation.\n%v", impIndex, bidder, err)}
+			if !cfg.SkipBidderParams {
+				if err := srv.paramsValidator.Validate(coreBidderNormalized, ext); err != nil {
+					return []error{fmt.Errorf("request.imp[%d].ext.prebid.bidder.%s failed validation.\n%v", impIndex, bidder, err)}
+				}
 			}
 		} else {
 			if msg, isDisabled := srv.disabledBidders[bidder]; isDisabled {
diff --git a/util/jsonutil/merge.go b/util/jsonutil/merge.go
index a104cf95..e1d69cf1 100644
--- a/util/jsonutil/merge.go
+++ b/util/jsonutil/merge.go
@@ -11,7 +11,6 @@ import (
 	jsonpatch "gopkg.in/evanphx/json-patch.v4"
 
 	"github.com/prebid/prebid-server/v2/errortypes"
-	"github.com/prebid/prebid-server/v2/util/reflectutil"
 )
 
 // jsonConfigMergeClone uses the same configuration as the `ConfigCompatibleWithStandardLibrary` profile
@@ -98,20 +97,14 @@ type sliceCloneDecoder struct {
 }
 
 func (d *sliceCloneDecoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
-	// don't clone if field is being set to nil. checking for nil "consumes" the null
-	// token, so must be handled in this decoder.
+	// clear the field. a new slice will be created by the original decoder if needed.
+	d.sliceType.UnsafeSetNil(ptr)
+
+	// checking for nil "consumes" the null token, so must be handled in this decoder.
 	if iter.ReadNil() {
-		d.sliceType.UnsafeSetNil(ptr)
 		return
 	}
 
-	// clone if there is an existing object. creation of new objects is handled by the
-	// original decoder.
-	if !d.sliceType.UnsafeIsNil(ptr) {
-		clone := reflectutil.UnsafeSliceClone(ptr, d.sliceType)
-		d.sliceType.UnsafeSet(ptr, clone)
-	}
-
 	d.valueDecoder.Decode(ptr, iter)
 }
 
diff --git a/util/reflectutil/slice.go b/util/reflectutil/slice.go
deleted file mode 100644
index 6da1aed2..00000000
--- a/util/reflectutil/slice.go
+++ /dev/null
@@ -1,49 +0,0 @@
-package reflectutil
-
-import (
-	"unsafe"
-
-	"github.com/modern-go/reflect2"
-)
-
-// UnsafeSliceClone clones an existing slice using unsafe.Pointer conventions. Intended
-// for use by json iterator extensions and should likely be used no where else. Nil
-// behavior is undefined as checks are expected upstream.
-func UnsafeSliceClone(ptr unsafe.Pointer, sliceType reflect2.SliceType) unsafe.Pointer {
-	// it's also possible to use `sliceType.Elem().RType`, but that returns a `uintptr`
-	// which causes `go vet` to emit a warning even though the usage is safe. this approach
-	// of copying some internals from the reflect2 package avoids the cast of `uintptr` to
-	// `unsafe.Pointer` which keeps `go vet` output clean.
-	elemRType := unpackEFace(sliceType.Elem().Type1()).data
-
-	header := (*sliceHeader)(ptr)
-	newHeader := (*sliceHeader)(sliceType.UnsafeMakeSlice(header.Len, header.Cap))
-	typedslicecopy(elemRType, *newHeader, *header)
-	return unsafe.Pointer(newHeader)
-}
-
-// sliceHeader is copied from the reflect2 package v1.0.2.
-type sliceHeader struct {
-	Data unsafe.Pointer
-	Len  int
-	Cap  int
-}
-
-// typedslicecopyis copied from the reflect2 package v1.0.2.
-// it copies a slice of elemType values from src to dst,
-// returning the number of elements copied.
-//
-//go:linkname typedslicecopy reflect.typedslicecopy
-//go:noescape
-func typedslicecopy(elemType unsafe.Pointer, dst, src sliceHeader) int
-
-// eface is copied from the reflect2 package v1.0.2.
-type eface struct {
-	rtype unsafe.Pointer
-	data  unsafe.Pointer
-}
-
-// unpackEFace is copied from the reflect2 package v1.0.2.
-func unpackEFace(obj interface{}) *eface {
-	return (*eface)(unsafe.Pointer(&obj))
-}


Test patch:
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index 162f4255..cdd2fc18 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -6349,6 +6349,6 @@ type mockRequestValidator struct {
 	errors []error
 }
 
-func (mrv *mockRequestValidator) ValidateImp(imp *openrtb_ext.ImpWrapper, index int, aliases map[string]string, hasStoredResponses bool, storedBidResponses stored_responses.ImpBidderStoredResp) []error {
+func (mrv *mockRequestValidator) ValidateImp(imp *openrtb_ext.ImpWrapper, cfg ortb.ValidationConfig, index int, aliases map[string]string, hasStoredResponses bool, storedBidResponses stored_responses.ImpBidderStoredResp) []error {
 	return mrv.errors
 }
diff --git a/exchange/exchangetest/imp-fpd-multiple-imp.json b/exchange/exchangetest/imp-fpd-multiple-imp.json
index 902a071b..11261cd1 100644
--- a/exchange/exchangetest/imp-fpd-multiple-imp.json
+++ b/exchange/exchangetest/imp-fpd-multiple-imp.json
@@ -83,7 +83,7 @@
                         }]
                     },
                     "native": {
-                        "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"data\":{\"type\":6}},{\"id\":5,\"data\":{\"type\":7}},{\"id\":6,\"data\":{\"type\":1,\"len\":20}}]}",
+                        "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}}]}",
                         "ver": "1.1"
                     },
                     "ext": {
@@ -98,7 +98,9 @@
                                     "audio": {
                                         "maxduration": 20,
                                         "durfloors": [{
-                                            "maxdur": 30
+                                            "mindur": 2,
+                                            "maxdur": 30,
+                                            "bidfloor": 20.0
                                         }],
                                         "poddur": 40
                                     },
@@ -164,14 +166,14 @@
                                 "minduration": 1,
                                 "maxduration": 20,
                                 "durfloors": [{
-                                    "mindur": 1,
+                                    "mindur": 2,
                                     "maxdur": 30,
-                                    "bidfloor": 10.0
+                                    "bidfloor": 20.0
                                 }],
                                 "poddur": 40
                             },
                             "native": {
-                                "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmttype\":4,\"plcmtcnt\":6,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":492,\"hmin\":328,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"data\":{\"type\":6}},{\"id\":5,\"data\":{\"type\":7}},{\"id\":6,\"data\":{\"type\":1,\"len\":20}}]}",
+                                "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}}]}",
                                 "ver": "2.2",
                                 "ctype": 1
                             },
diff --git a/exchange/exchangetest/imp-fpd-skipped-validation.json b/exchange/exchangetest/imp-fpd-skipped-validation.json
new file mode 100644
index 00000000..d6285a02
--- /dev/null
+++ b/exchange/exchangetest/imp-fpd-skipped-validation.json
@@ -0,0 +1,137 @@
+{
+    "requestType": "openrtb2-web",
+    "incomingRequest": {
+        "ortbRequest": {
+            "id": "some-request-id",
+            "site": {
+                "page": "test.somepage.com"
+            },
+            "imp": [
+                {
+                    "id": "imp-id-1",
+                    "native": {},
+                    "ext": {
+                        "prebid": {
+                            "bidder": {
+                                "appnexus": {
+                                    "placementId": []
+                                }
+                            },
+                            "imp": {
+                                "appnexus": {
+                                    "id": "imp-id-1-appnexus"
+                                }
+                            }
+                        }
+                    }
+                },
+                {
+                    "id": "imp-id-2",
+                    "banner": {
+                        "format": [
+                            {
+                                "w": 300,
+                                "h": 600
+                            }
+                        ]
+                    },
+                    "ext": {
+                        "prebid": {
+                            "bidder": {
+                                "appnexus": {
+                                    "placementId": "123"
+                                }
+                            }
+                        }
+                    }
+                }
+            ]
+        }
+    },
+    "outgoingRequests": {
+        "appnexus": {
+            "expectRequest": {
+                "ortbRequest": {
+                    "id": "some-request-id",
+                    "site": {
+                        "page": "test.somepage.com"
+                    },
+                    "imp": [
+                        {
+                            "id": "imp-id-1-appnexus",
+                            "native": {},
+                            "ext": {
+                                "bidder": {
+                                    "placementId": []
+                                }
+                            }
+                        },
+                        {
+                            "id": "imp-id-2",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 300,
+                                        "h": 600
+                                    }
+                                ]
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "placementId": "123"
+                                }
+                            }
+                        }
+                    ]
+                }
+            },
+            "mockResponse": {
+                "pbsSeatBids": [
+                    {
+                        "pbsBids": [
+                            {
+                                "ortbBid": {
+                                    "id": "apn-bid",
+                                    "impid": "imp-id-2",
+                                    "price": 0.3,
+                                    "w": 300,
+                                    "h": 600,
+                                    "crid": "creative-1"
+                                },
+                                "bidType": "banner"
+                            }
+                        ],
+                        "seat": "appnexus"
+                    }
+                ]
+            }
+        }
+    },
+    "response": {
+        "bids": {
+            "id": "some-request-id",
+            "seatbid": [
+                {
+                    "seat": "appnexus",
+                    "bid": [
+                        {
+                            "id": "apn-bid",
+                            "impid": "imp-id-2",
+                            "price": 0.3,
+                            "w": 300,
+                            "h": 600,
+                            "crid": "creative-1",
+                            "ext": {
+                                "origbidcpm": 0.3,
+                                "prebid": {
+                                    "meta": {},
+                                    "type": "banner"
+                                }
+                            }
+                        }
+                    ]
+                }
+            ]
+        }
+    }
+}
\ No newline at end of file
diff --git a/exchange/utils_test.go b/exchange/utils_test.go
index c22fc091..69cfdf70 100644
--- a/exchange/utils_test.go
+++ b/exchange/utils_test.go
@@ -230,7 +230,7 @@ func TestSplitImps(t *testing.T) {
 							},
 						},
 					},
-					Ext: json.RawMessage(`{"prebid":{"bidder":{"bidderA":{"imp1paramA":"imp1valueA"}},"imp":{"bidderA":{"id":"impFPD", "banner":{"format":[{"h":30}]}}}}}`),
+					Ext: json.RawMessage(`{"prebid":{"bidder":{"bidderA":{"imp1paramA":"imp1valueA"}},"imp":{"bidderA":{"id":"impFPD", "banner":{"format":[{"w":30,"h":40}]}}}}}`),
 				},
 			},
 			expectedImps: map[string][]openrtb2.Imp{
@@ -240,8 +240,8 @@ func TestSplitImps(t *testing.T) {
 						Banner: &openrtb2.Banner{
 							Format: []openrtb2.Format{
 								{
-									W: 10,
-									H: 30,
+									W: 30,
+									H: 40,
 								},
 							},
 						},
@@ -264,7 +264,7 @@ func TestSplitImps(t *testing.T) {
 							},
 						},
 					},
-					Ext: json.RawMessage(`{"prebid":{"bidder":{"bidderB":{"imp1paramB":"imp1valueB"}},"imp":{"bidderA":{"id":"impFPD", "banner":{"format":[{"h":30}]}}}}}`),
+					Ext: json.RawMessage(`{"prebid":{"bidder":{"bidderB":{"imp1paramB":"imp1valueB"}},"imp":{"bidderA":{"id":"impFPD", "banner":{"format":[{"w":30,"h":40}]}}}}}`),
 				},
 			},
 			expectedImps: map[string][]openrtb2.Imp{
@@ -463,7 +463,7 @@ func TestMergeImpFPD(t *testing.T) {
 						{
 							W:   10,
 							H:   20,
-							Ext: json.RawMessage(`{"formatkey1":"formatval1", "formatkey2":"formatval2"}`),
+							Ext: json.RawMessage(`{"formatkey1":"formatval1"}`),
 						},
 					},
 				},
@@ -475,7 +475,7 @@ func TestMergeImpFPD(t *testing.T) {
 				IframeBuster: []string{"buster1", "buster2"},
 				Ext:          json.RawMessage(`{"cool1":"test1", "cool2":"test2"}`),
 			},
-			fpd: json.RawMessage(`{"id": "id2", "metric": [{"type":"type2", "value":2, "vendor":"vendor2"}], "banner": {"w":100, "format": [{"w":1000, "ext":{"formatkey1":"formatval11"}}]}, "instl":2, "bidfloor":2, "ext":{"cool1":"test11"} }`),
+			fpd: json.RawMessage(`{"id": "id2", "metric": [{"type":"type2", "value":2, "vendor":"vendor2"}], "banner": {"w":100, "format": [{"w":1000, "h":2000, "ext":{"formatkey1":"formatval11"}}]}, "instl":2, "bidfloor":2, "ext":{"cool1":"test11"} }`),
 			wantImp: &openrtb2.Imp{
 				ID:     "id2",
 				Metric: []openrtb2.Metric{{Type: "type2", Value: 2, Vendor: "vendor2"}},
@@ -485,8 +485,8 @@ func TestMergeImpFPD(t *testing.T) {
 					Format: []openrtb2.Format{
 						{
 							W:   1000,
-							H:   20,
-							Ext: json.RawMessage(`{"formatkey1":"formatval11","formatkey2":"formatval2"}`),
+							H:   2000,
+							Ext: json.RawMessage(`{"formatkey1":"formatval11"}`),
 						},
 					},
 				},
diff --git a/ortb/request_validator_test.go b/ortb/request_validator_test.go
index 1c84a6ca..1eb0708f 100644
--- a/ortb/request_validator_test.go
+++ b/ortb/request_validator_test.go
@@ -13,10 +13,12 @@ import (
 
 func TestValidateImpExt(t *testing.T) {
 	type testCase struct {
-		description    string
-		impExt         json.RawMessage
-		expectedImpExt string
-		expectedErrs   []error
+		description         string
+		impExt              json.RawMessage
+		cfg                 ValidationConfig
+		paramValidatorError error
+		expectedImpExt      string
+		expectedErrs        []error
 	}
 	testGroups := []struct {
 		description string
@@ -200,6 +202,31 @@ func TestValidateImpExt(t *testing.T) {
 				},
 			},
 		},
+		{
+			"Config tests",
+			[]testCase{
+				{
+					description: "Invalid Params",
+					impExt:      json.RawMessage(`{"appnexus":{"placement_id_wrong_format":[]}}`),
+					cfg: ValidationConfig{
+						SkipBidderParams: false,
+					},
+					paramValidatorError: errors.New("params error"),
+					expectedImpExt:      `{"appnexus":{"placement_id_wrong_format":[]}}`,
+					expectedErrs:        []error{errors.New("request.imp[0].ext.prebid.bidder.appnexus failed validation.\nparams error")},
+				},
+				{
+					description: "Invalid Params - Skip Params Validation",
+					impExt:      json.RawMessage(`{"appnexus":{"placement_id_wrong_format":[]}}`),
+					cfg: ValidationConfig{
+						SkipBidderParams: true,
+					},
+					paramValidatorError: errors.New("params error"),
+					expectedImpExt:      `{"prebid":{"bidder":{"appnexus":{"placement_id_wrong_format":[]}}}}`,
+					expectedErrs:        []error{},
+				},
+			},
+		},
 	}
 
 	for _, group := range testGroups {
@@ -212,9 +239,11 @@ func TestValidateImpExt(t *testing.T) {
 				rv := standardRequestValidator{
 					bidderMap:       openrtb_ext.BuildBidderMap(),
 					disabledBidders: disabledBidders,
-					paramsValidator: mockBidderParamValidator{},
+					paramsValidator: mockBidderParamValidator{
+						Error: test.paramValidatorError,
+					},
 				}
-				errs := rv.validateImpExt(impWrapper, nil, 0, false, nil)
+				errs := rv.validateImpExt(impWrapper, test.cfg, nil, 0, false, nil)
 
 				assert.NoError(t, impWrapper.RebuildImp(), test.description+":rebuild_imp")
 
@@ -229,9 +258,11 @@ func TestValidateImpExt(t *testing.T) {
 	}
 }
 
-type mockBidderParamValidator struct{}
+type mockBidderParamValidator struct {
+	Error error
+}
 
 func (v mockBidderParamValidator) Validate(name openrtb_ext.BidderName, ext json.RawMessage) error {
-	return nil
+	return v.Error
 }
 func (v mockBidderParamValidator) Schema(name openrtb_ext.BidderName) string { return "" }
diff --git a/util/jsonutil/merge_test.go b/util/jsonutil/merge_test.go
index 8c85d623..217a980e 100644
--- a/util/jsonutil/merge_test.go
+++ b/util/jsonutil/merge_test.go
@@ -139,6 +139,22 @@ func TestMergeCloneSlice(t *testing.T) {
 		assert.Equal(t, []string{"b", "c"}, imp.IframeBuster, "new-val")
 	})
 
+	t.Run("replace-not-overlay", func(t *testing.T) {
+		var (
+			imp      = &openrtb2.Imp{ID: "1"}
+			impSlice = []*openrtb2.Imp{imp}
+			test     = &struct {
+				Imps []*openrtb2.Imp `json:"imps"`
+			}{Imps: impSlice}
+		)
+
+		err := MergeClone(test, []byte(`{"imps":[{"tagid":"2"}]}`))
+		require.NoError(t, err)
+
+		impExpected := &openrtb2.Imp{TagID: "2"} // ensure original id is no longer present
+		assert.Equal(t, []*openrtb2.Imp{impExpected}, test.Imps)
+	})
+
 	t.Run("invalid-null", func(t *testing.T) {
 		var (
 			iframeBuster = []string{"a"}
diff --git a/util/reflectutil/slice_test.go b/util/reflectutil/slice_test.go
deleted file mode 100644
index d5183892..00000000
--- a/util/reflectutil/slice_test.go
+++ /dev/null
@@ -1,78 +0,0 @@
-package reflectutil
-
-import (
-	"testing"
-
-	"github.com/modern-go/reflect2"
-	"github.com/stretchr/testify/assert"
-)
-
-func TestUnsafeSliceCloneSimple(t *testing.T) {
-	testCases := []struct {
-		name  string
-		given []int
-	}{
-		{
-			name:  "empty",
-			given: []int{},
-		},
-		{
-			name:  "one",
-			given: []int{1},
-		},
-		{
-			name:  "many",
-			given: []int{1, 2},
-		},
-	}
-
-	for _, test := range testCases {
-		t.Run(test.name, func(t *testing.T) {
-			original := test.given
-			clonePtr := UnsafeSliceClone(reflect2.PtrOf(test.given), reflect2.TypeOf([]int{}).(*reflect2.UnsafeSliceType))
-			clone := *(*[]int)(clonePtr)
-
-			assert.NotSame(t, original, clone, "reference")
-			assert.Equal(t, original, clone, "equality")
-			assert.Equal(t, len(original), len(clone), "len")
-			assert.Equal(t, cap(original), cap(clone), "cap")
-		})
-	}
-}
-
-func TestUnsafeSliceCloneComplex(t *testing.T) {
-	type foo struct {
-		value string
-	}
-
-	testCases := []struct {
-		name  string
-		given []foo
-	}{
-		{
-			name:  "empty",
-			given: []foo{},
-		},
-		{
-			name:  "one",
-			given: []foo{{value: "a"}},
-		},
-		{
-			name:  "many",
-			given: []foo{{value: "a"}, {value: "b"}},
-		},
-	}
-
-	for _, test := range testCases {
-		t.Run(test.name, func(t *testing.T) {
-			original := test.given
-			clonePtr := UnsafeSliceClone(reflect2.PtrOf(test.given), reflect2.TypeOf([]foo{}).(*reflect2.UnsafeSliceType))
-			clone := *(*[]foo)(clonePtr)
-
-			assert.NotSame(t, original, clone, "reference")
-			assert.Equal(t, original, clone, "equality")
-			assert.Equal(t, len(original), len(clone), "len")
-			assert.Equal(t, cap(original), cap(clone), "cap")
-		})
-	}
-}