Repository: prebid/prebid-server
Pull request #1132 (2019-12-04 18:52:25)

Better native request validation
We were requiring some fields that are only "recommended", and we have seen examples in the wild where they were not filled. This relaxes those requirements.


Patch:
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index fd180359..a2a9cf94 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -444,6 +444,10 @@ func fillAndValidateNative(n *openrtb.Native, impIndex int) error {
 }
 
 func validateNativeContextTypes(cType native.ContextType, cSubtype native.ContextSubType, impIndex int) error {
+	if cType == 0 {
+		// Context is only recommended, so none is a valid type.
+		return nil
+	}
 	if cType < native.ContextTypeContent || cType > native.ContextTypeProduct {
 		return fmt.Errorf("request.imp[%d].native.request.context is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", impIndex)
 	}
@@ -480,6 +484,10 @@ func validateNativeContextTypes(cType native.ContextType, cSubtype native.Contex
 }
 
 func validateNativePlacementType(pt native.PlacementType, impIndex int) error {
+	if pt == 0 {
+		// Placement Type is only reccomended, not required.
+		return nil
+	}
 	if pt < native.PlacementTypeFeed || pt > native.PlacementTypeRecommendationWidget {
 		return fmt.Errorf("request.imp[%d].native.request.plcmttype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40", impIndex)
 	}
@@ -523,9 +531,7 @@ func validateNativeAsset(asset nativeRequests.Asset, impIndex int, assetIndex in
 			return fmt.Errorf(assetErr, impIndex, assetIndex)
 		}
 		foundType = true
-		if err := validateNativeAssetImg(asset.Img, impIndex, assetIndex); err != nil {
-			return err
-		}
+		// It is technically valid to have neither w/h nor wmin/hmin, so no check
 	}
 
 	if asset.Video != nil {
@@ -587,19 +593,6 @@ func validateNativeEventTracker(tracker nativeRequests.EventTracker, impIndex in
 	return nil
 }
 
-func validateNativeAssetImg(image *nativeRequests.Image, impIndex int, assetIndex int) error {
-	// Note that w, wmin, h, and hmin cannot be negative because these variables use unsigned ints.
-	// Those fail during the standard json.Unmarshal() call.
-	if image.W == 0 && image.WMin == 0 {
-		return fmt.Errorf(`request.imp[%d].native.request.assets[%d].img must contain at least one of "w" or "wmin"`, impIndex, assetIndex)
-	}
-	if image.H == 0 && image.HMin == 0 {
-		return fmt.Errorf(`request.imp[%d].native.request.assets[%d].img must contain at least one of "h" or "hmin"`, impIndex, assetIndex)
-	}
-
-	return nil
-}
-
 func validateNativeAssetVideo(video *nativeRequests.Video, impIndex int, assetIndex int) error {
 	if len(video.MIMEs) < 1 {
 		return fmt.Errorf("request.imp[%d].native.request.assets[%d].video.mimes must be an array with at least one MIME type", impIndex, assetIndex)
diff --git a/endpoints/openrtb2/sample-requests/invalid-native/asset-img-no-hmin.json b/endpoints/openrtb2/sample-requests/valid-native/asset-img-no-hmin.json
similarity index 100%
rename from endpoints/openrtb2/sample-requests/invalid-native/asset-img-no-hmin.json
rename to endpoints/openrtb2/sample-requests/valid-native/asset-img-no-hmin.json
diff --git a/endpoints/openrtb2/sample-requests/invalid-native/asset-img-no-wmin.json b/endpoints/openrtb2/sample-requests/valid-native/asset-img-no-wmin.json
similarity index 100%
rename from endpoints/openrtb2/sample-requests/invalid-native/asset-img-no-wmin.json
rename to endpoints/openrtb2/sample-requests/valid-native/asset-img-no-wmin.json
diff --git a/endpoints/openrtb2/sample-requests/invalid-native/request-no-context.json b/endpoints/openrtb2/sample-requests/valid-native/request-no-context.json
similarity index 100%
rename from endpoints/openrtb2/sample-requests/invalid-native/request-no-context.json
rename to endpoints/openrtb2/sample-requests/valid-native/request-no-context.json
diff --git a/endpoints/openrtb2/sample-requests/invalid-native/request-no-plcmttype.json b/endpoints/openrtb2/sample-requests/valid-native/request-plcmttype-empty.json
similarity index 100%
rename from endpoints/openrtb2/sample-requests/invalid-native/request-no-plcmttype.json
rename to endpoints/openrtb2/sample-requests/valid-native/request-plcmttype-empty.json
diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go
index 61f4f3e7..1f234b85 100644
--- a/endpoints/openrtb2/video_auction.go
+++ b/endpoints/openrtb2/video_auction.go
@@ -91,7 +91,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 	}
 	requestJson, err := ioutil.ReadAll(lr)
 	if err != nil {
-		handleError(labels, w, []error{err}, ao)
+		handleError(&labels, w, []error{err}, &ao)
 		return
 	}
 
@@ -102,27 +102,27 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 
 	if err != nil {
 		if deps.cfg.VideoStoredRequestRequired {
-			handleError(labels, w, []error{err}, ao)
+			handleError(&labels, w, []error{err}, &ao)
 			return
 		}
 	} else {
 		storedRequest, errs := deps.loadStoredVideoRequest(context.Background(), storedRequestId)
 		if len(errs) > 0 {
-			handleError(labels, w, errs, ao)
+			handleError(&labels, w, errs, &ao)
 			return
 		}
 
 		//merge incoming req with stored video req
 		resolvedRequest, err = jsonpatch.MergePatch(storedRequest, requestJson)
 		if err != nil {
-			handleError(labels, w, []error{err}, ao)
+			handleError(&labels, w, []error{err}, &ao)
 			return
 		}
 	}
 	//unmarshal and validate combined result
 	videoBidReq, errL, podErrors := deps.parseVideoRequest(resolvedRequest)
 	if len(errL) > 0 {
-		handleError(labels, w, errL, ao)
+		handleError(&labels, w, errL, &ao)
 		return
 	}
 
@@ -130,7 +130,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 	if deps.defaultRequest {
 		if err := json.Unmarshal(deps.defReqJSON, bidReq); err != nil {
 			err = fmt.Errorf("Invalid JSON in Default Request Settings: %s", err)
-			handleError(labels, w, []error{err}, ao)
+			handleError(&labels, w, []error{err}, &ao)
 			return
 		}
 	}
@@ -154,7 +154,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 		}
 		err := errors.New(fmt.Sprintf("all pods are incorrect: %s", strings.Join(resPodErr, "; ")))
 		errL = append(errL, err)
-		handleError(labels, w, errL, ao)
+		handleError(&labels, w, errL, &ao)
 		return
 	}
 
@@ -166,7 +166,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 
 	errL = deps.validateRequest(bidReq)
 	if len(errL) > 0 {
-		handleError(labels, w, errL, ao)
+		handleError(&labels, w, errL, &ao)
 		return
 	}
 
@@ -194,7 +194,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 
 	if acctIdErr := validateAccount(deps.cfg, labels.PubID); acctIdErr != nil {
 		errL = append(errL, acctIdErr)
-		handleError(labels, w, errL, ao)
+		handleError(&labels, w, errL, &ao)
 		return
 	}
 	//execute auction logic
@@ -203,7 +203,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 	ao.Response = response
 	if err != nil {
 		errL := []error{err}
-		handleError(labels, w, errL, ao)
+		handleError(&labels, w, errL, &ao)
 		return
 	}
 
@@ -211,7 +211,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 	bidResp, err := buildVideoResponse(response, podErrors)
 	if err != nil {
 		errL := []error{err}
-		handleError(labels, w, errL, ao)
+		handleError(&labels, w, errL, &ao)
 		return
 	}
 	if bidReq.Test == 1 {
@@ -222,7 +222,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 	//resp, err := json.Marshal(response)
 	if err != nil {
 		errL := []error{err}
-		handleError(labels, w, errL, ao)
+		handleError(&labels, w, errL, &ao)
 		return
 	}
 
@@ -238,7 +238,7 @@ func cleanupVideoBidRequest(videoReq *openrtb_ext.BidRequestVideo, podErrors []P
 	return videoReq
 }
 
-func handleError(labels pbsmetrics.Labels, w http.ResponseWriter, errL []error, ao analytics.AuctionObject) {
+func handleError(labels *pbsmetrics.Labels, w http.ResponseWriter, errL []error, ao *analytics.AuctionObject) {
 	labels.RequestStatus = pbsmetrics.RequestStatusErr
 	var errors string
 	var status int = http.StatusInternalServerError
diff --git a/exchange/exchange.go b/exchange/exchange.go
index 76a4d302..e92e813f 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -615,11 +615,11 @@ func (e *exchange) getBidCacheInfo(bid *pbsOrtbBid, auc *auction) (openrtb_ext.E
 		if cacheUUID, found = auc.cacheIds[bid.bid]; found {
 			cacheInfo.CacheId = cacheUUID
 			extCacheHost, extCachePath = e.cache.GetExtCacheData()
-			cacheInfo.Url = extCacheHost + "/" + extCachePath + "?uuid=" + cacheUUID
+			cacheInfo.Url = extCacheHost + extCachePath + "?uuid=" + cacheUUID
 		} else if cacheUUID, found = auc.vastCacheIds[bid.bid]; found {
 			cacheInfo.CacheId = cacheUUID
 			extCacheHost, extCachePath = e.cache.GetExtCacheData()
-			cacheInfo.Url = extCacheHost + "/" + extCachePath + "?uuid=" + cacheUUID
+			cacheInfo.Url = extCacheHost + extCachePath + "?uuid=" + cacheUUID
 		}
 	}
 	return cacheInfo, found
diff --git a/prebid_cache_client/client.go b/prebid_cache_client/client.go
index 88da5c3a..6da69f68 100644
--- a/prebid_cache_client/client.go
+++ b/prebid_cache_client/client.go
@@ -72,9 +72,11 @@ type clientImpl struct {
 func (c *clientImpl) GetExtCacheData() (string, string) {
 	path := c.externalCachePath
 	if path == "/" {
+		// Only the slash for the path, remove it to empty
 		path = ""
-	} else if strings.Index(path, "/") == 0 {
-		path = strings.TrimLeft(path, "/")
+	} else if len(path) > 0 && !strings.HasPrefix(path, "/") {
+		// Path defined but does not start with "/", prepend it
+		path = "/" + path
 	}
 
 	return c.externalCacheHost, path


Test patch:
diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go
index 4818c556..7f3d3d4c 100644
--- a/endpoints/openrtb2/auction_test.go
+++ b/endpoints/openrtb2/auction_test.go
@@ -282,7 +282,6 @@ func TestRejectAccountRequired(t *testing.T) {
 func (gr *getResponseFromDirectory) assert(t *testing.T) {
 	//t *testing.T, dir string, payloadGetter func(*testing.T, []byte) []byte, messageGetter func(*testing.T, []byte) []byte, expectedCode int, aliased bool) {
 	t.Helper()
-	var filename string
 	var filesToAssert []string
 	if gr.file == "" {
 		// Append every file found in `gr.dir` to the `filesToAssert` array and test them all
@@ -300,14 +299,14 @@ func (gr *getResponseFromDirectory) assert(t *testing.T) {
 		fileData = readFile(t, testFile)
 		code, msg := gr.doRequest(t, gr.payloadGetter(t, fileData))
 		fmt.Printf("Processing %s\n", testFile)
-		assertResponseCode(t, filename, code, gr.expectedCode, msg)
+		assertResponseCode(t, testFile, code, gr.expectedCode, msg)
 
 		expectMsg := gr.messageGetter(t, fileData)
 		if gr.description != "" {
 			if len(expectMsg) > 0 {
-				assert.Equal(t, string(expectMsg), msg, "Test failed. %s. Filename: \n", gr.description, filename)
+				assert.Equal(t, string(expectMsg), msg, "Test failed. %s. Filename: \n", gr.description, testFile)
 			} else {
-				assert.Equal(t, string(expectMsg), msg, "file %s had bad response body", filename)
+				assert.Equal(t, string(expectMsg), msg, "file %s had bad response body", testFile)
 			}
 		}
 	}
diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go
index 85285a30..6a8e913a 100644
--- a/endpoints/openrtb2/video_auction_test.go
+++ b/endpoints/openrtb2/video_auction_test.go
@@ -3,12 +3,14 @@ package openrtb2
 import (
 	"context"
 	"encoding/json"
+	"errors"
 	"io/ioutil"
 	"net/http/httptest"
 	"strings"
 	"testing"
 
 	"github.com/mxmCherry/openrtb"
+	"github.com/prebid/prebid-server/analytics"
 	analyticsConf "github.com/prebid/prebid-server/analytics/config"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/exchange"
@@ -641,6 +643,91 @@ func TestMergeOpenRTBToVideoRequest(t *testing.T) {
 	assert.Equal(t, videoReq.Site.Page, bidReq.Site.Page, "Device.Site.Page is incorrect")
 }
 
+func TestHandleError(t *testing.T) {
+	ao := analytics.AuctionObject{
+		Status: 200,
+		Errors: make([]error, 0),
+	}
+
+	labels := pbsmetrics.Labels{
+		Source:        pbsmetrics.DemandUnknown,
+		RType:         pbsmetrics.ReqTypeVideo,
+		PubID:         pbsmetrics.PublisherUnknown,
+		Browser:       "test browser",
+		CookieFlag:    pbsmetrics.CookieFlagUnknown,
+		RequestStatus: pbsmetrics.RequestStatusOK,
+	}
+
+	recorder := httptest.NewRecorder()
+	err1 := errors.New("Error for testing handleError 1")
+	err2 := errors.New("Error for testing handleError 2")
+	handleError(&labels, recorder, []error{err1, err2}, &ao)
+
+	assert.Equal(t, pbsmetrics.RequestStatusErr, labels.RequestStatus, "labels.RequestStatus should indicate an error")
+	assert.Equal(t, 500, recorder.Code, "Error status should be written to writer")
+	assert.Equal(t, 500, ao.Status, "AnalyticsObject should have error status")
+	assert.Equal(t, 2, len(ao.Errors), "New errors should be appended to AnalyticsObject Errors")
+	assert.Equal(t, "Error for testing handleError 1", ao.Errors[0].Error(), "Error in AnalyticsObject should have test error message for first error")
+	assert.Equal(t, "Error for testing handleError 2", ao.Errors[1].Error(), "Error in AnalyticsObject should have test error message for second error")
+}
+
+func TestHandleErrorMetrics(t *testing.T) {
+	ex := &mockExchangeVideo{}
+	reqData, err := ioutil.ReadFile("sample-requests/video/video_invalid_sample.json")
+	if err != nil {
+		t.Fatalf("Failed to fetch a valid request: %v", err)
+	}
+	reqBody := string(getRequestPayload(t, reqData))
+	req := httptest.NewRequest("POST", "/openrtb2/video", strings.NewReader(reqBody))
+	recorder := httptest.NewRecorder()
+
+	deps, met, mod := mockDepsWithMetrics(t, ex)
+	deps.VideoAuctionEndpoint(recorder, req, nil)
+
+	assert.Equal(t, int64(0), met.RequestStatuses[pbsmetrics.ReqTypeVideo][pbsmetrics.RequestStatusOK].Count(), "OK requests count should be 0")
+	assert.Equal(t, int64(1), met.RequestStatuses[pbsmetrics.ReqTypeVideo][pbsmetrics.RequestStatusErr].Count(), "Error requests count should be 1")
+	assert.Equal(t, 1, len(mod.auctionObjects), "Mock AnalyticsModule should have 1 AuctionObject")
+	assert.Equal(t, 500, mod.auctionObjects[0].Status, "AnalyticsObject should have 500 status")
+	assert.Equal(t, 2, len(mod.auctionObjects[0].Errors), "AnalyticsObject should have Errors length of 2")
+	assert.Equal(t, "request missing required field: PodConfig.DurationRangeSec", mod.auctionObjects[0].Errors[0].Error(), "First error in AnalyticsObject should have message regarding DurationRangeSec")
+	assert.Equal(t, "request missing required field: PodConfig.Pods", mod.auctionObjects[0].Errors[1].Error(), "Second error in AnalyticsObject should have message regarding Pods")
+}
+
+func mockDepsWithMetrics(t *testing.T, ex *mockExchangeVideo) (*endpointDeps, *pbsmetrics.Metrics, *mockAnalyticsModule) {
+	theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{})
+	mockModule := &mockAnalyticsModule{}
+	edep := &endpointDeps{
+		ex,
+		newParamsValidator(t),
+		&mockVideoStoredReqFetcher{},
+		&mockVideoStoredReqFetcher{},
+		empty_fetcher.EmptyFetcher{},
+		&config.Configuration{MaxRequestSize: maxSize},
+		theMetrics,
+		mockModule,
+		map[string]string{},
+		false,
+		[]byte{},
+		openrtb_ext.BidderMap,
+	}
+
+	return edep, theMetrics, mockModule
+}
+
+type mockAnalyticsModule struct {
+	auctionObjects []*analytics.AuctionObject
+}
+
+func (m *mockAnalyticsModule) LogAuctionObject(ao *analytics.AuctionObject) {
+	m.auctionObjects = append(m.auctionObjects, ao)
+}
+
+func (m *mockAnalyticsModule) LogCookieSyncObject(cso *analytics.CookieSyncObject) { return }
+
+func (m *mockAnalyticsModule) LogSetUIDObject(so *analytics.SetUIDObject) { return }
+
+func (m *mockAnalyticsModule) LogAmpObject(ao *analytics.AmpObject) { return }
+
 func mockDeps(t *testing.T, ex *mockExchangeVideo) *endpointDeps {
 	theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{})
 	edep := &endpointDeps{
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index f5bf23d5..2b0e37e6 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -1291,7 +1291,7 @@ func mockHandler(statusCode int, getBody string, postBody string) http.Handler {
 type wellBehavedCache struct{}
 
 func (c *wellBehavedCache) GetExtCacheData() (string, string) {
-	return "www.pbcserver.com", "pbcache/endpoint"
+	return "www.pbcserver.com", "/pbcache/endpoint"
 }
 
 func (c *wellBehavedCache) PutJson(ctx context.Context, values []prebid_cache_client.Cacheable) ([]string, []error) {
diff --git a/exchange/exchangetest/include-brand-category.json b/exchange/exchangetest/include-brand-category.json
index f27c0e6d..68e93e3a 100644
--- a/exchange/exchangetest/include-brand-category.json
+++ b/exchange/exchangetest/include-brand-category.json
@@ -106,8 +106,8 @@
                      "hb_bidder_appnexus": "appnexus",
                      "hb_cache_host": "www.pbcserver.com",
                      "hb_cache_host_appnex": "www.pbcserver.com",
-                     "hb_cache_path": "pbcache/endpoint",
-                     "hb_cache_path_appnex": "pbcache/endpoint",
+                     "hb_cache_path": "/pbcache/endpoint",
+                     "hb_cache_path_appnex": "/pbcache/endpoint",
                      "hb_pb": "0.20",
                      "hb_pb_appnexus": "0.20",
                      "hb_pb_cat_dur": "0.20_VideoGames_0s",
@@ -128,8 +128,8 @@
                       "hb_bidder_appnexus": "appnexus",
                       "hb_cache_host": "www.pbcserver.com",
                       "hb_cache_host_appnex": "www.pbcserver.com",
-                      "hb_cache_path": "pbcache/endpoint",
-                      "hb_cache_path_appnex": "pbcache/endpoint",
+                      "hb_cache_path": "/pbcache/endpoint",
+                      "hb_cache_path_appnex": "/pbcache/endpoint",
                       "hb_pb": "0.50",
                       "hb_pb_appnexus": "0.50",
                       "hb_pb_cat_dur": "0.50_HomeDecor_0s",
diff --git a/exchange/exchangetest/targeting-cache-vast-banner.json b/exchange/exchangetest/targeting-cache-vast-banner.json
index 5d0db274..215485cc 100644
--- a/exchange/exchangetest/targeting-cache-vast-banner.json
+++ b/exchange/exchangetest/targeting-cache-vast-banner.json
@@ -71,8 +71,8 @@
                   "hb_bidder_appnexus": "appnexus",
                   "hb_cache_host": "www.pbcserver.com",
                   "hb_cache_host_appnex": "www.pbcserver.com",
-                  "hb_cache_path": "pbcache/endpoint",
-                  "hb_cache_path_appnex": "pbcache/endpoint",
+                  "hb_cache_path": "/pbcache/endpoint",
+                  "hb_cache_path_appnex": "/pbcache/endpoint",
                   "hb_pb": "0.00",
                   "hb_pb_appnexus": "0.00",
                   "hb_size": "200x250",
diff --git a/exchange/exchangetest/targeting-cache-vast.json b/exchange/exchangetest/targeting-cache-vast.json
index 743f2ce3..f348dd1b 100644
--- a/exchange/exchangetest/targeting-cache-vast.json
+++ b/exchange/exchangetest/targeting-cache-vast.json
@@ -78,8 +78,8 @@
                   "hb_bidder_appnexus": "appnexus",
                   "hb_cache_host": "www.pbcserver.com",
                   "hb_cache_host_appnex": "www.pbcserver.com",
-                  "hb_cache_path": "pbcache/endpoint",
-                  "hb_cache_path_appnex": "pbcache/endpoint",
+                  "hb_cache_path": "/pbcache/endpoint",
+                  "hb_cache_path_appnex": "/pbcache/endpoint",
                   "hb_uuid": "0",
                   "hb_uuid_appnexus": "0",
                   "hb_pb": "0.00",
diff --git a/exchange/exchangetest/targeting-cache-zero.json b/exchange/exchangetest/targeting-cache-zero.json
index abe9df10..51301530 100644
--- a/exchange/exchangetest/targeting-cache-zero.json
+++ b/exchange/exchangetest/targeting-cache-zero.json
@@ -83,8 +83,8 @@
                   "hb_cache_host_appnex": "www.pbcserver.com",
                   "hb_cache_id": "0",
                   "hb_cache_id_appnexus": "0",
-                  "hb_cache_path": "pbcache/endpoint",
-                  "hb_cache_path_appnex": "pbcache/endpoint",
+                  "hb_cache_path": "/pbcache/endpoint",
+                  "hb_cache_path_appnex": "/pbcache/endpoint",
                   "hb_uuid": "1",
                   "hb_uuid_appnexus": "1",
                   "hb_pb": "0.00",
diff --git a/exchange/exchangetest/targeting-mobile.json b/exchange/exchangetest/targeting-mobile.json
index d5661565..f3962cff 100644
--- a/exchange/exchangetest/targeting-mobile.json
+++ b/exchange/exchangetest/targeting-mobile.json
@@ -123,7 +123,7 @@
                 "targeting": {
                   "hb_bidder_audienceNe": "audienceNetwork",
                   "hb_cache_host_audien": "www.pbcserver.com",
-                  "hb_cache_path_audien": "pbcache/endpoint",
+                  "hb_cache_path_audien": "/pbcache/endpoint",
                   "hb_pb_audienceNetwor": "0.50",
                   "hb_size_audienceNetw": "200x250",
                   "hb_env_audienceNetwo": "mobile-app"
@@ -148,8 +148,8 @@
                   "hb_bidder": "appnexus",
                   "hb_cache_host": "www.pbcserver.com",
                   "hb_cache_host_appnex": "www.pbcserver.com",
-                  "hb_cache_path": "pbcache/endpoint",
-                  "hb_cache_path_appnex": "pbcache/endpoint",
+                  "hb_cache_path": "/pbcache/endpoint",
+                  "hb_cache_path_appnex": "/pbcache/endpoint",
                   "hb_bidder_appnexus": "appnexus",
                   "hb_pb": "0.70",
                   "hb_pb_appnexus": "0.70",
@@ -188,8 +188,8 @@
                   "hb_bidder": "appnexus",
                   "hb_cache_host": "www.pbcserver.com",
                   "hb_cache_host_appnex": "www.pbcserver.com",
-                  "hb_cache_path": "pbcache/endpoint",
-                  "hb_cache_path_appnex": "pbcache/endpoint",
+                  "hb_cache_path": "/pbcache/endpoint",
+                  "hb_cache_path_appnex": "/pbcache/endpoint",
                   "hb_bidder_appnexus": "appnexus",
                   "hb_pb": "0.60",
                   "hb_pb_appnexus": "0.60",
diff --git a/exchange/exchangetest/targeting-no-winners.json b/exchange/exchangetest/targeting-no-winners.json
index 73bf812c..891ff0ff 100644
--- a/exchange/exchangetest/targeting-no-winners.json
+++ b/exchange/exchangetest/targeting-no-winners.json
@@ -125,7 +125,7 @@
                 "targeting": {
                   "hb_bidder_audienceNe": "audienceNetwork",
                   "hb_cache_host_audien": "www.pbcserver.com",
-                  "hb_cache_path_audien": "pbcache/endpoint",
+                  "hb_cache_path_audien": "/pbcache/endpoint",
                   "hb_pb_audienceNetwor": "0.50",
                   "hb_size_audienceNetw": "200x250"
                 }
@@ -148,7 +148,7 @@
                 "targeting": {
                   "hb_bidder_appnexus": "appnexus",
                   "hb_cache_host_appnex": "www.pbcserver.com",
-                  "hb_cache_path_appnex": "pbcache/endpoint",
+                  "hb_cache_path_appnex": "/pbcache/endpoint",
                   "hb_pb_appnexus": "0.70",
                   "hb_size_appnexus": "200x250"
                 }
@@ -181,7 +181,7 @@
                 "targeting": {
                   "hb_bidder_appnexus": "appnexus",
                   "hb_cache_host_appnex": "www.pbcserver.com",
-                  "hb_cache_path_appnex": "pbcache/endpoint",
+                  "hb_cache_path_appnex": "/pbcache/endpoint",
                   "hb_pb_appnexus": "0.60",
                   "hb_size_appnexus": "300x500"
                 }
diff --git a/exchange/exchangetest/targeting-only-winners.json b/exchange/exchangetest/targeting-only-winners.json
index 807b7b6c..5f449af3 100644
--- a/exchange/exchangetest/targeting-only-winners.json
+++ b/exchange/exchangetest/targeting-only-winners.json
@@ -141,7 +141,7 @@
                 "targeting": {
                   "hb_bidder": "appnexus",
                   "hb_cache_host": "www.pbcserver.com",
-                  "hb_cache_path": "pbcache/endpoint",
+                  "hb_cache_path": "/pbcache/endpoint",
                   "hb_pb": "0.70",
                   "hb_size": "200x250"
                 }
@@ -174,7 +174,7 @@
                 "targeting": {
                   "hb_bidder": "appnexus",
                   "hb_cache_host": "www.pbcserver.com",
-                  "hb_cache_path": "pbcache/endpoint",
+                  "hb_cache_path": "/pbcache/endpoint",
                   "hb_pb": "0.60",
                   "hb_size": "300x500"
                 }
diff --git a/exchange/exchangetest/targeting-with-winners.json b/exchange/exchangetest/targeting-with-winners.json
index 208b3feb..f081b7ee 100644
--- a/exchange/exchangetest/targeting-with-winners.json
+++ b/exchange/exchangetest/targeting-with-winners.json
@@ -123,7 +123,7 @@
                 "targeting": {
                   "hb_bidder_audienceNe": "audienceNetwork",
                   "hb_cache_host_audien": "www.pbcserver.com",
-                  "hb_cache_path_audien": "pbcache/endpoint",
+                  "hb_cache_path_audien": "/pbcache/endpoint",
                   "hb_pb_audienceNetwor": "0.50",
                   "hb_size_audienceNetw": "200x250"
                 }
@@ -148,8 +148,8 @@
                   "hb_bidder_appnexus": "appnexus",
                   "hb_cache_host": "www.pbcserver.com",
                   "hb_cache_host_appnex": "www.pbcserver.com",
-                  "hb_cache_path": "pbcache/endpoint",
-                  "hb_cache_path_appnex": "pbcache/endpoint",
+                  "hb_cache_path": "/pbcache/endpoint",
+                  "hb_cache_path_appnex": "/pbcache/endpoint",
                   "hb_pb": "0.70",
                   "hb_pb_appnexus": "0.70",
                   "hb_size": "200x250",
@@ -186,8 +186,8 @@
                   "hb_bidder_appnexus": "appnexus",
                   "hb_cache_host": "www.pbcserver.com",
                   "hb_cache_host_appnex": "www.pbcserver.com",
-                  "hb_cache_path": "pbcache/endpoint",
-                  "hb_cache_path_appnex": "pbcache/endpoint",
+                  "hb_cache_path": "/pbcache/endpoint",
+                  "hb_cache_path_appnex": "/pbcache/endpoint",
                   "hb_pb": "0.60",
                   "hb_pb_appnexus": "0.60",
                   "hb_size": "300x500",
diff --git a/prebid_cache_client/client_test.go b/prebid_cache_client/client_test.go
index cbeaa38a..d3b5ee4b 100644
--- a/prebid_cache_client/client_test.go
+++ b/prebid_cache_client/client_test.go
@@ -156,10 +156,10 @@ func TestStripCacheHostAndPath(t *testing.T) {
 		{
 			inExtCacheURL: config.ExternalCache{
 				Host: "prebid-server.prebid.org",
-				Path: "pbcache/endpoint",
+				Path: "/pbcache/endpoint",
 			},
 			expectedHost: "prebid-server.prebid.org",
-			expectedPath: "pbcache/endpoint",
+			expectedPath: "/pbcache/endpoint",
 		},
 		{
 			inExtCacheURL: config.ExternalCache{
@@ -177,6 +177,22 @@ func TestStripCacheHostAndPath(t *testing.T) {
 			expectedHost: "",
 			expectedPath: "",
 		},
+		{
+			inExtCacheURL: config.ExternalCache{
+				Host: "prebid-server.prebid.org",
+				Path: "pbcache/endpoint",
+			},
+			expectedHost: "prebid-server.prebid.org",
+			expectedPath: "/pbcache/endpoint",
+		},
+		{
+			inExtCacheURL: config.ExternalCache{
+				Host: "prebidcache.net",
+				Path: "/",
+			},
+			expectedHost: "prebidcache.net",
+			expectedPath: "",
+		},
 	}
 	for _, test := range testInput {
 		//start client