diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go index c93c03e85e3..0604e703c01 100644 --- a/endpoints/openrtb2/amp_auction.go +++ b/endpoints/openrtb2/amp_auction.go @@ -35,12 +35,12 @@ type AmpResponse struct { // NewAmpEndpoint modifies the OpenRTB endpoint to handle AMP requests. This will basically modify the parsing // of the request, and the return value, using the OpenRTB machinery to handle everything inbetween. -func NewAmpEndpoint(ex exchange.Exchange, validator openrtb_ext.BidderParamValidator, requestsById stored_requests.Fetcher, cfg *config.Configuration, met pbsmetrics.MetricsEngine, pbsAnalytics analytics.PBSAnalyticsModule) (httprouter.Handle, error) { +func NewAmpEndpoint(ex exchange.Exchange, validator openrtb_ext.BidderParamValidator, requestsById stored_requests.Fetcher, cfg *config.Configuration, met pbsmetrics.MetricsEngine, pbsAnalytics analytics.PBSAnalyticsModule, disabledBidders map[string]string) (httprouter.Handle, error) { if ex == nil || validator == nil || requestsById == nil || cfg == nil || met == nil { return nil, errors.New("NewAmpEndpoint requires non-nil arguments.") } - return httprouter.Handle((&endpointDeps{ex, validator, requestsById, cfg, met, pbsAnalytics}).AmpAuction), nil + return httprouter.Handle((&endpointDeps{ex, validator, requestsById, cfg, met, pbsAnalytics, disabledBidders}).AmpAuction), nil } func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { @@ -95,7 +95,7 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h req, errL := deps.parseAmpRequest(r) - if len(errL) > 0 { + if fatalError(errL) { w.WriteHeader(http.StatusBadRequest) for _, err := range errL { w.Write([]byte(fmt.Sprintf("Invalid request format: %s\n", err.Error()))) @@ -227,10 +227,11 @@ func (deps *endpointDeps) parseAmpRequest(httpRequest *http.Request) (req *openr // At this point, we should have a valid request that definitely has Targeting and Cache turned on - if err := deps.validateRequest(req); err != nil { - errs = []error{err} - return + errL := deps.validateRequest(req) + if len(errL) > 0 { + errs = append(errs, errL...) } + return } diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go index b1cca1039d5..12bbbb9fd74 100644 --- a/endpoints/openrtb2/amp_auction_test.go +++ b/endpoints/openrtb2/amp_auction_test.go @@ -40,7 +40,7 @@ func TestGoodAmpRequests(t *testing.T) { // NewMetrics() will create a new go_metrics MetricsEngine, bypassing the need for a crafted configuration set to support it. // As a side effect this gives us some coverage of the go_metrics piece of the metrics engine. theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList()) - endpoint, _ := NewAmpEndpoint(&mockAmpExchange{}, newParamsValidator(t), &mockAmpStoredReqFetcher{goodRequests}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{})) + endpoint, _ := NewAmpEndpoint(&mockAmpExchange{}, newParamsValidator(t), &mockAmpStoredReqFetcher{goodRequests}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}) for requestID := range goodRequests { request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=%s", requestID), nil) @@ -82,7 +82,7 @@ func TestAMPPageInfo(t *testing.T) { } theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList()) exchange := &mockAmpExchange{} - endpoint, _ := NewAmpEndpoint(exchange, newParamsValidator(t), &mockAmpStoredReqFetcher{stored}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{})) + endpoint, _ := NewAmpEndpoint(exchange, newParamsValidator(t), &mockAmpStoredReqFetcher{stored}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}) request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=1&curl=%s", url.QueryEscape(page)), nil) recorder := httptest.NewRecorder() endpoint(recorder, request, nil) @@ -131,7 +131,7 @@ func TestAmpBadRequests(t *testing.T) { // NewMetrics() will create a new go_metrics MetricsEngine, bypassing the need for a crafted configuration set to support it. // As a side effect this gives us some coverage of the go_metrics piece of the metrics engine. theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList()) - endpoint, _ := NewEndpoint(&mockAmpExchange{}, newParamsValidator(t), &mockAmpStoredReqFetcher{badRequests}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{})) + endpoint, _ := NewEndpoint(&mockAmpExchange{}, newParamsValidator(t), &mockAmpStoredReqFetcher{badRequests}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}) for requestID := range badRequests { request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=%s", requestID), nil) recorder := httptest.NewRecorder() @@ -151,7 +151,7 @@ func TestAmpDebug(t *testing.T) { } theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList()) - endpoint, _ := NewAmpEndpoint(&mockAmpExchange{}, newParamsValidator(t), &mockAmpStoredReqFetcher{requests}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{})) + endpoint, _ := NewAmpEndpoint(&mockAmpExchange{}, newParamsValidator(t), &mockAmpStoredReqFetcher{requests}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}) for requestID := range requests { request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=%s&debug=1", requestID), nil) @@ -212,7 +212,7 @@ func TestQueryParamOverrides(t *testing.T) { "1": json.RawMessage(validRequest(t, "site.json")), } theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList()) - endpoint, _ := NewAmpEndpoint(&mockAmpExchange{}, newParamsValidator(t), &mockAmpStoredReqFetcher{requests}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{})) + endpoint, _ := NewAmpEndpoint(&mockAmpExchange{}, newParamsValidator(t), &mockAmpStoredReqFetcher{requests}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}) requestID := "1" curl := "http://example.com" @@ -329,7 +329,7 @@ func (s formatOverrideSpec) execute(t *testing.T) { "1": json.RawMessage(validRequest(t, "site.json")), } theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList()) - endpoint, _ := NewAmpEndpoint(&mockAmpExchange{}, newParamsValidator(t), &mockAmpStoredReqFetcher{requests}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{})) + endpoint, _ := NewAmpEndpoint(&mockAmpExchange{}, newParamsValidator(t), &mockAmpStoredReqFetcher{requests}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}) url := fmt.Sprintf("/openrtb2/auction/amp?tag_id=1&debug=1&w=%d&h=%d&ow=%d&oh=%d&ms=%s", s.width, s.height, s.overrideWidth, s.overrideHeight, s.multisize) request := httptest.NewRequest("GET", url, nil) diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index 111454217e1..9283dfbccf4 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -22,6 +22,7 @@ import ( nativeRequests "github.com/mxmCherry/openrtb/native/request" "github.com/prebid/prebid-server/analytics" "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/errortypes" "github.com/prebid/prebid-server/exchange" "github.com/prebid/prebid-server/openrtb_ext" "github.com/prebid/prebid-server/pbsmetrics" @@ -33,12 +34,12 @@ import ( const storedRequestTimeoutMillis = 50 -func NewEndpoint(ex exchange.Exchange, validator openrtb_ext.BidderParamValidator, requestsById stored_requests.Fetcher, cfg *config.Configuration, met pbsmetrics.MetricsEngine, pbsAnalytics analytics.PBSAnalyticsModule) (httprouter.Handle, error) { +func NewEndpoint(ex exchange.Exchange, validator openrtb_ext.BidderParamValidator, requestsById stored_requests.Fetcher, cfg *config.Configuration, met pbsmetrics.MetricsEngine, pbsAnalytics analytics.PBSAnalyticsModule, disabledBidders map[string]string) (httprouter.Handle, error) { if ex == nil || validator == nil || requestsById == nil || cfg == nil || met == nil { return nil, errors.New("NewEndpoint requires non-nil arguments.") } - return httprouter.Handle((&endpointDeps{ex, validator, requestsById, cfg, met, pbsAnalytics}).Auction), nil + return httprouter.Handle((&endpointDeps{ex, validator, requestsById, cfg, met, pbsAnalytics, disabledBidders}).Auction), nil } type endpointDeps struct { @@ -48,6 +49,7 @@ type endpointDeps struct { cfg *config.Configuration metricsEngine pbsmetrics.MetricsEngine analytics analytics.PBSAnalyticsModule + disabledBidders map[string]string } func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { @@ -87,7 +89,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http req, errL := deps.parseRequest(r) - if writeError(errL, w) { + if fatalError(errL) && writeError(errL, w) { labels.RequestStatus = pbsmetrics.RequestStatusBadInput return } @@ -201,9 +203,9 @@ func (deps *endpointDeps) parseRequest(httpRequest *http.Request) (req *openrtb. // Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers). deps.setFieldsImplicitly(httpRequest, req) - if err := deps.validateRequest(req); err != nil { - errs = []error{err} - return + errL := deps.validateRequest(req) + if len(errL) > 0 { + errs = append(errs, errL...) } return @@ -224,57 +226,66 @@ func parseTimeout(requestJson []byte, defaultTimeout time.Duration) time.Duratio return defaultTimeout } -func (deps *endpointDeps) validateRequest(req *openrtb.BidRequest) error { +func (deps *endpointDeps) validateRequest(req *openrtb.BidRequest) []error { + errL := []error{} if req.ID == "" { - return errors.New("request missing required field: \"id\"") + return []error{errors.New("request missing required field: \"id\"")} } if req.TMax < 0 { - return fmt.Errorf("request.tmax must be nonnegative. Got %d", req.TMax) + return []error{fmt.Errorf("request.tmax must be nonnegative. Got %d", req.TMax)} } if len(req.Imp) < 1 { - return errors.New("request.imp must contain at least one element.") + return []error{errors.New("request.imp must contain at least one element.")} } var aliases map[string]string if bidExt, err := deps.parseBidExt(req.Ext); err != nil { - return err + return []error{err} } else if bidExt != nil { aliases = bidExt.Prebid.Aliases if err := deps.validateAliases(aliases); err != nil { - return err + return []error{err} } if err := validateBidAdjustmentFactors(bidExt.Prebid.BidAdjustmentFactors, aliases); err != nil { - return err + return []error{err} } } for index, imp := range req.Imp { - if err := deps.validateImp(&imp, aliases, index); err != nil { - return err + errs := deps.validateImp(&imp, aliases, index) + if len(errs) > 0 { + errL = append(errL, errs...) + } + if fatalError(errs) { + return errL } } if (req.Site == nil && req.App == nil) || (req.Site != nil && req.App != nil) { - return errors.New("request.site or request.app must be defined, but not both.") + errL = append(errL, errors.New("request.site or request.app must be defined, but not both.")) + return errL } if err := deps.validateSite(req.Site); err != nil { - return err + errL = append(errL, err) + return errL } if err := validateUser(req.User, aliases); err != nil { - return err + errL = append(errL, err) + return errL } if err := validateRegs(req.Regs); err != nil { - return err + errL = append(errL, err) + return errL } - return nil + return errL } func validateBidAdjustmentFactors(adjustmentFactors map[string]float64, aliases map[string]string) error { @@ -291,45 +302,46 @@ func validateBidAdjustmentFactors(adjustmentFactors map[string]float64, aliases return nil } -func (deps *endpointDeps) validateImp(imp *openrtb.Imp, aliases map[string]string, index int) error { +func (deps *endpointDeps) validateImp(imp *openrtb.Imp, aliases map[string]string, index int) []error { if imp.ID == "" { - return fmt.Errorf("request.imp[%d] missing required field: \"id\"", index) + return []error{fmt.Errorf("request.imp[%d] missing required field: \"id\"", index)} } if len(imp.Metric) != 0 { - return fmt.Errorf("request.imp[%d].metric is not yet supported by prebid-server. Support may be added in the future", index) + return []error{fmt.Errorf("request.imp[%d].metric is not yet supported by prebid-server. Support may be added in the future", index)} } if imp.Banner == nil && imp.Video == nil && imp.Audio == nil && imp.Native == nil { - return fmt.Errorf("request.imp[%d] must contain at least one of \"banner\", \"video\", \"audio\", or \"native\"", index) + return []error{fmt.Errorf("request.imp[%d] must contain at least one of \"banner\", \"video\", \"audio\", or \"native\"", index)} } if err := validateBanner(imp.Banner, index); err != nil { - return err + return []error{err} } if imp.Video != nil { if len(imp.Video.MIMEs) < 1 { - return fmt.Errorf("request.imp[%d].video.mimes must contain at least one supported MIME type", index) + return []error{fmt.Errorf("request.imp[%d].video.mimes must contain at least one supported MIME type", index)} } } if imp.Audio != nil { if len(imp.Audio.MIMEs) < 1 { - return fmt.Errorf("request.imp[%d].audio.mimes must contain at least one supported MIME type", index) + return []error{fmt.Errorf("request.imp[%d].audio.mimes must contain at least one supported MIME type", index)} } } if err := fillAndValidateNative(imp.Native, index); err != nil { - return err + return []error{err} } if err := validatePmp(imp.PMP, index); err != nil { - return err + return []error{err} } - if err := deps.validateImpExt(imp.Ext, aliases, index); err != nil { - return err + errL := deps.validateImpExt(imp, aliases, index) + if len(errL) != 0 { + return errL } return nil @@ -630,19 +642,17 @@ func validatePmp(pmp *openrtb.PMP, impIndex int) error { return nil } -func (deps *endpointDeps) validateImpExt(ext json.RawMessage, aliases map[string]string, impIndex int) error { - if len(ext) == 0 { - return fmt.Errorf("request.imp[%d].ext is required", impIndex) +func (deps *endpointDeps) validateImpExt(imp *openrtb.Imp, aliases map[string]string, impIndex int) []error { + errL := []error{} + if len(imp.Ext) == 0 { + return []error{fmt.Errorf("request.imp[%d].ext is required", impIndex)} } var bidderExts map[string]json.RawMessage - if err := json.Unmarshal(ext, &bidderExts); err != nil { - return err - } - - if len(bidderExts) < 1 { - return fmt.Errorf("request.imp[%d].ext must contain at least one bidder", impIndex) + if err := json.Unmarshal(imp.Ext, &bidderExts); err != nil { + return []error{err} } + disabledBidders := []string{} for bidder, ext := range bidderExts { if bidder != "prebid" { coreBidder := bidder @@ -651,14 +661,37 @@ func (deps *endpointDeps) validateImpExt(ext json.RawMessage, aliases map[string } if bidderName, isValid := openrtb_ext.BidderMap[coreBidder]; isValid { if err := deps.paramsValidator.Validate(bidderName, ext); err != nil { - return fmt.Errorf("request.imp[%d].ext.%s failed validation.\n%v", impIndex, coreBidder, err) + return []error{fmt.Errorf("request.imp[%d].ext.%s failed validation.\n%v", impIndex, coreBidder, err)} } } else { - return fmt.Errorf("request.imp[%d].ext contains unknown bidder: %s. Did you forget an alias in request.ext.prebid.aliases?", impIndex, bidder) + if msg, isDisabled := deps.disabledBidders[bidder]; isDisabled { + errL = append(errL, &errortypes.BidderTemporarilyDisabled{Message: msg}) + disabledBidders = append(disabledBidders, bidder) + } else { + return []error{fmt.Errorf("request.imp[%d].ext contains unknown bidder: %s. Did you forget an alias in request.ext.prebid.aliases?", impIndex, bidder)} + } } } } + // defer deleting disabled bidders so we don't disrupt the loop + if len(disabledBidders) > 0 { + for _, bidder := range disabledBidders { + delete(bidderExts, bidder) + } + extJSON, err := json.Marshal(bidderExts) + if err != nil { + return []error{err} + } + imp.Ext = extJSON + } + + // TODO #713 Fix this here + if len(bidderExts) < 1 { + errL = append(errL, fmt.Errorf("request.imp[%d].ext must contain at least one bidder", impIndex)) + return errL + } + return nil } @@ -982,3 +1015,13 @@ func writeError(errs []error, w http.ResponseWriter) bool { } return false } + +// Checks to see if an error in an error list is a fatal error +func fatalError(errL []error) bool { + for _, err := range errL { + if errortypes.DecodeError(err) != errortypes.BidderTemporarilyDisabledCode { + return true + } + } + return false +} diff --git a/endpoints/openrtb2/auction_benchmark_test.go b/endpoints/openrtb2/auction_benchmark_test.go index 673ee0e420c..a5f708d16aa 100644 --- a/endpoints/openrtb2/auction_benchmark_test.go +++ b/endpoints/openrtb2/auction_benchmark_test.go @@ -67,7 +67,7 @@ func BenchmarkOpenrtbEndpoint(b *testing.B) { if err != nil { return } - endpoint, _ := NewEndpoint(exchange.NewExchange(server.Client(), nil, &config.Configuration{}, theMetrics, infos, gdpr.AlwaysAllow{}), paramValidator, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{})) + endpoint, _ := NewEndpoint(exchange.NewExchange(server.Client(), nil, &config.Configuration{}, theMetrics, infos, gdpr.AlwaysAllow{}), paramValidator, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}) b.ResetTimer() for n := 0; n < b.N; n++ { diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index e8ddb0180f8..a54bd7d2c6a 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -82,7 +82,7 @@ func TestExplicitUserId(t *testing.T) { // NewMetrics() will create a new go_metrics MetricsEngine, bypassing the need for a crafted configuration set to support it. // As a side effect this gives us some coverage of the go_metrics piece of the metrics engine. theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList()) - endpoint, _ := NewEndpoint(ex, newParamsValidator(t), empty_fetcher.EmptyFetcher{}, cfg, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{})) + endpoint, _ := NewEndpoint(ex, newParamsValidator(t), empty_fetcher.EmptyFetcher{}, cfg, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}) endpoint(httptest.NewRecorder(), request, nil) if ex.lastRequest == nil { @@ -118,7 +118,7 @@ func TestImplicitUserId(t *testing.T) { // NewMetrics() will create a new go_metrics MetricsEngine, bypassing the need for a crafted configuration set to support it. // As a side effect this gives us some coverage of the go_metrics piece of the metrics engine. theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList()) - endpoint, _ := NewEndpoint(ex, newParamsValidator(t), empty_fetcher.EmptyFetcher{}, cfg, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{})) + endpoint, _ := NewEndpoint(ex, newParamsValidator(t), empty_fetcher.EmptyFetcher{}, cfg, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}) endpoint(httptest.NewRecorder(), request, nil) if ex.lastRequest == nil { @@ -195,7 +195,7 @@ func doRequest(t *testing.T, requestData []byte) (int, string) { // NewMetrics() will create a new go_metrics MetricsEngine, bypassing the need for a crafted configuration set to support it. // As a side effect this gives us some coverage of the go_metrics piece of the metrics engine. theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList()) - endpoint, _ := NewEndpoint(&nobidExchange{}, newParamsValidator(t), empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{})) + endpoint, _ := NewEndpoint(&nobidExchange{}, newParamsValidator(t), empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}) request := httptest.NewRequest("POST", "/openrtb2/auction", bytes.NewReader(requestData)) recorder := httptest.NewRecorder() @@ -256,7 +256,7 @@ func TestNilExchange(t *testing.T) { // NewMetrics() will create a new go_metrics MetricsEngine, bypassing the need for a crafted configuration set to support it. // As a side effect this gives us some coverage of the go_metrics piece of the metrics engine. theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList()) - _, err := NewEndpoint(nil, newParamsValidator(t), empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{})) + _, err := NewEndpoint(nil, newParamsValidator(t), empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}) if err == nil { t.Errorf("NewEndpoint should return an error when given a nil Exchange.") } @@ -267,7 +267,7 @@ func TestNilValidator(t *testing.T) { // NewMetrics() will create a new go_metrics MetricsEngine, bypassing the need for a crafted configuration set to support it. // As a side effect this gives us some coverage of the go_metrics piece of the metrics engine. theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList()) - _, err := NewEndpoint(&nobidExchange{}, nil, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{})) + _, err := NewEndpoint(&nobidExchange{}, nil, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}) if err == nil { t.Errorf("NewEndpoint should return an error when given a nil BidderParamValidator.") } @@ -278,7 +278,7 @@ func TestExchangeError(t *testing.T) { // NewMetrics() will create a new go_metrics MetricsEngine, bypassing the need for a crafted configuration set to support it. // As a side effect this gives us some coverage of the go_metrics piece of the metrics engine. theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList()) - endpoint, _ := NewEndpoint(&brokenExchange{}, newParamsValidator(t), empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{})) + endpoint, _ := NewEndpoint(&brokenExchange{}, newParamsValidator(t), empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}) request := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, "site.json"))) recorder := httptest.NewRecorder() endpoint(recorder, request, nil) @@ -336,7 +336,7 @@ func TestImplicitIPs(t *testing.T) { // NewMetrics() will create a new go_metrics MetricsEngine, bypassing the need for a crafted configuration set to support it. // As a side effect this gives us some coverage of the go_metrics piece of the metrics engine. theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList()) - endpoint, _ := NewEndpoint(ex, newParamsValidator(t), &mockStoredReqFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{})) + endpoint, _ := NewEndpoint(ex, newParamsValidator(t), &mockStoredReqFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}) httpReq := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, "site.json"))) httpReq.Header.Set("X-Forwarded-For", "123.456.78.90") recorder := httptest.NewRecorder() @@ -392,7 +392,7 @@ func TestStoredRequests(t *testing.T) { // NewMetrics() will create a new go_metrics MetricsEngine, bypassing the need for a crafted configuration set to support it. // As a side effect this gives us some coverage of the go_metrics piece of the metrics engine. theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList()) - edep := &endpointDeps{&nobidExchange{}, newParamsValidator(t), &mockStoredReqFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{})} + edep := &endpointDeps{&nobidExchange{}, newParamsValidator(t), &mockStoredReqFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}} for i, requestData := range testStoredRequests { newRequest, errList := edep.processStoredRequests(context.Background(), json.RawMessage(requestData)) @@ -422,6 +422,7 @@ func TestOversizedRequest(t *testing.T) { &config.Configuration{MaxRequestSize: int64(len(reqBody) - 1)}, pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList()), analyticsConf.NewPBSAnalytics(&config.Analytics{}), + map[string]string{}, } req := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(reqBody)) @@ -448,6 +449,7 @@ func TestRequestSizeEdgeCase(t *testing.T) { &config.Configuration{MaxRequestSize: int64(len(reqBody))}, pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList()), analyticsConf.NewPBSAnalytics(&config.Analytics{}), + map[string]string{}, } req := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(reqBody)) @@ -472,7 +474,8 @@ func TestNoEncoding(t *testing.T) { &mockStoredReqFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList()), - analyticsConf.NewPBSAnalytics(&config.Analytics{})) + analyticsConf.NewPBSAnalytics(&config.Analytics{}), + map[string]string{}) request := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, "site.json"))) recorder := httptest.NewRecorder() endpoint(recorder, request, nil) @@ -542,7 +545,8 @@ func TestContentType(t *testing.T) { &mockStoredReqFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList()), - analyticsConf.NewPBSAnalytics(&config.Analytics{})) + analyticsConf.NewPBSAnalytics(&config.Analytics{}), + map[string]string{}) request := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, "site.json"))) recorder := httptest.NewRecorder() endpoint(recorder, request, nil) @@ -552,6 +556,38 @@ func TestContentType(t *testing.T) { } } +// TestDisabledBidder makes sure we pass when encountering a disabled bidder in the configuration. +func TestDisabledBidder(t *testing.T) { + reqData, err := ioutil.ReadFile("sample-requests/invalid-whole/unknown-bidder.json") + if err != nil { + t.Fatalf("Failed to fetch a valid request: %v", err) + } + reqBody := string(getRequestPayload(t, reqData)) + + deps := &endpointDeps{ + &nobidExchange{}, + newParamsValidator(t), + &mockStoredReqFetcher{}, + &config.Configuration{MaxRequestSize: int64(len(reqBody))}, + pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList()), + analyticsConf.NewPBSAnalytics(&config.Analytics{}), + map[string]string{"unknownbidder": "The biddder 'unknownbidder' has been disabled."}, + } + + req := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(reqBody)) + recorder := httptest.NewRecorder() + + deps.Auction(recorder, req, nil) + + if recorder.Code != http.StatusOK { + t.Errorf("Endpoint should return a 200 if the unknown bidder was disabled.") + } + + if bytesRead, err := req.Body.Read(make([]byte, 1)); bytesRead != 0 || err != io.EOF { + t.Errorf("The request body should have been read to completion.") + } +} + func validRequest(t *testing.T, filename string) string { requestData, err := ioutil.ReadFile("sample-requests/valid-whole/supplementary/" + filename) if err != nil { diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/unknown-bidder.json b/endpoints/openrtb2/sample-requests/invalid-whole/unknown-bidder.json new file mode 100644 index 00000000000..f11be6fd51c --- /dev/null +++ b/endpoints/openrtb2/sample-requests/invalid-whole/unknown-bidder.json @@ -0,0 +1,38 @@ +{ + "description": "Copy of the prebid test ad, with the addition of an unknown bidder", + + "message": "Invalid request: request.imp[0].ext contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?\n", + "requestPayload": { + "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": 10433394 + }, + "unknownbidder": { + "param1": "foobar", + "param2": 42 + } + } + } + ], + "tmax": 500 + } + } \ No newline at end of file diff --git a/errortypes/errortypes.go b/errortypes/errortypes.go index d51b25436f0..01c78b1d3d7 100644 --- a/errortypes/errortypes.go +++ b/errortypes/errortypes.go @@ -8,6 +8,7 @@ const ( BadInputCode BadServerResponseCode FailedToRequestBidsCode + BidderTemporarilyDisabledCode ) // We should use this code for any Error interface that is not in this package @@ -88,6 +89,21 @@ func (err *FailedToRequestBids) Code() int { return FailedToRequestBidsCode } +// BidderTemporarilyDisabled is used at the request validation step, where we want to continue processing as best we +// can rather than returning a 4xx, and still return an error message. +// The initial usecase is to flag deprecated bidders. +type BidderTemporarilyDisabled struct { + Message string +} + +func (err *BidderTemporarilyDisabled) Error() string { + return err.Message +} + +func (err *BidderTemporarilyDisabled) Code() int { + return BidderTemporarilyDisabledCode +} + // DecodeError provides the error code for an error, as defined above func DecodeError(err error) int { if ce, ok := err.(Coder); ok { diff --git a/router/router.go b/router/router.go index 1c36eed9386..d904be623b4 100644 --- a/router/router.go +++ b/router/router.go @@ -160,6 +160,10 @@ type Router struct { func New(cfg *config.Configuration) (r *Router, err error) { const schemaDirectory = "./static/bidder-params" const infoDirectory = "./static/bidder-info" + disabledBidders := map[string]string{ + "indexExchange": "Bidder \"indexExchange\" has been deprecated and is no longer available. Please use bidder \"ix\" and note that the bidder params have changed.", + } + r = &Router{ Router: httprouter.New(), } @@ -201,12 +205,12 @@ func New(cfg *config.Configuration) (r *Router, err error) { exchanges = newExchangeMap(cfg) theExchange := exchange.NewExchange(theClient, pbc.NewClient(&cfg.CacheURL), cfg, r.MetricsEngine, bidderInfos, gdprPerms) - openrtbEndpoint, err := openrtb2.NewEndpoint(theExchange, paramsValidator, fetcher, cfg, r.MetricsEngine, pbsAnalytics) + openrtbEndpoint, err := openrtb2.NewEndpoint(theExchange, paramsValidator, fetcher, cfg, r.MetricsEngine, pbsAnalytics, disabledBidders) if err != nil { glog.Fatalf("Failed to create the openrtb endpoint handler. %v", err) } - ampEndpoint, err := openrtb2.NewAmpEndpoint(theExchange, paramsValidator, ampFetcher, cfg, r.MetricsEngine, pbsAnalytics) + ampEndpoint, err := openrtb2.NewAmpEndpoint(theExchange, paramsValidator, ampFetcher, cfg, r.MetricsEngine, pbsAnalytics, disabledBidders) if err != nil { glog.Fatalf("Failed to create the amp endpoint handler. %v", err) }