diff --git a/adapters/adpone/adpone.go b/adapters/adpone/adpone.go index 345a4988580..b1822a0ac07 100644 --- a/adapters/adpone/adpone.go +++ b/adapters/adpone/adpone.go @@ -3,9 +3,10 @@ package adpone import ( "encoding/json" "fmt" - "github.com/prebid/prebid-server/openrtb_ext" "net/http" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/mxmCherry/openrtb" "github.com/prebid/prebid-server/adapters" "github.com/prebid/prebid-server/errortypes" diff --git a/adapters/audienceNetwork/facebook.go b/adapters/audienceNetwork/facebook.go index 706673cbafc..3ece7bb99e4 100644 --- a/adapters/audienceNetwork/facebook.go +++ b/adapters/audienceNetwork/facebook.go @@ -447,3 +447,20 @@ func NewFacebookBidder(client *http.Client, platformID string, appSecret string) appSecret: appSecret, } } + +func (fa *FacebookAdapter) MakeTimeoutNotification(req *adapters.RequestData) (*adapters.RequestData, []error) { + // Note, facebook creates one request per imp, so all these requests will only have one imp in them + auction_id, err := jsonparser.GetString(req.Body, "imp", "[0]", "id") + if err != nil { + return &adapters.RequestData{}, []error{err} + } + + uri := fmt.Sprintf("https://www.facebook.com/audiencenetwork/nurl/?partner=%s&app=%s&auction=%s&ortb_loss_code=2", fa.platformID, fa.platformID, auction_id) + timeoutReq := adapters.RequestData{ + Method: "GET", + Uri: uri, + Body: nil, + Headers: http.Header{}, + } + return &timeoutReq, nil +} diff --git a/adapters/audienceNetwork/facebook_test.go b/adapters/audienceNetwork/facebook_test.go index 2ce0ef3ba64..1edaabd45d7 100644 --- a/adapters/audienceNetwork/facebook_test.go +++ b/adapters/audienceNetwork/facebook_test.go @@ -4,7 +4,9 @@ import ( "testing" "time" + "github.com/prebid/prebid-server/adapters" "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/stretchr/testify/assert" ) type tagInfo struct { @@ -40,3 +42,38 @@ type FacebookExt struct { func TestJsonSamples(t *testing.T) { adapterstest.RunJSONBidderTest(t, "audienceNetworktest", NewFacebookBidder(nil, "test-platform-id", "test-app-secret")) } + +func TestMakeTimeoutNotice(t *testing.T) { + req := adapters.RequestData{ + Body: []byte(`{"imp":[{"id":"1234"}]}}`), + } + fba := NewFacebookBidder(nil, "test-platform-id", "test-app-secret") + + tb, ok := fba.(adapters.TimeoutBidder) + if !ok { + t.Error("Facebook adapter is not a TimeoutAdapter") + } + + toReq, err := tb.MakeTimeoutNotification(&req) + assert.Nil(t, err, "Facebook MakeTimeoutNotification() return an error %v", err) + expectedUri := "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=test-platform-id&auction=1234&ortb_loss_code=2" + assert.Equal(t, expectedUri, toReq.Uri, "Facebook timeout notification not returning the expected URI.") + +} + +func TestMakeTimeoutNoticeBadRequest(t *testing.T) { + req := adapters.RequestData{ + Body: []byte(`{"imp":[{{"id":"1234"}}`), + } + fba := NewFacebookBidder(nil, "test-platform-id", "test-app-secret") + + tb, ok := fba.(adapters.TimeoutBidder) + if !ok { + t.Error("Facebook adapter is not a TimeoutAdapter") + } + + toReq, err := tb.MakeTimeoutNotification(&req) + assert.Empty(t, toReq.Uri, "Facebook MakeTimeoutNotification() did not return nil", err) + assert.NotNil(t, err, "Facebook MakeTimeoutNotification() did not return an error") + +} diff --git a/adapters/bidder.go b/adapters/bidder.go index 9d3ffb75414..baec4135b6a 100644 --- a/adapters/bidder.go +++ b/adapters/bidder.go @@ -39,6 +39,19 @@ type Bidder interface { MakeBids(internalRequest *openrtb.BidRequest, externalRequest *RequestData, response *ResponseData) (*BidderResponse, []error) } +// TimeoutBidder is used to identify bidders that support timeout notifications. +type TimeoutBidder interface { + Bidder + + // MakeTimeoutNotice functions much the same as MakeRequests, except it is fed the bidder request that timed out, + // and expects that only one notification "request" will be generated. A use case for multiple timeout notifications + // has not been anticipated. + // + // Do note that if MakeRequests returns multiple requests, and more than one of these times out, MakeTimeoutNotice will be called + // once for each timed out request. + MakeTimeoutNotification(req *RequestData) (*RequestData, []error) +} + type MisconfiguredBidder struct { Name string Error error diff --git a/exchange/bidder.go b/exchange/bidder.go index 5708660057f..d9a28fee175 100644 --- a/exchange/bidder.go +++ b/exchange/bidder.go @@ -8,6 +8,7 @@ import ( "fmt" "io/ioutil" "net/http" + "time" "github.com/mxmCherry/openrtb" nativeRequests "github.com/mxmCherry/openrtb/native/request" @@ -295,6 +296,14 @@ func (bidder *bidderAdapter) doRequest(ctx context.Context, req *adapters.Reques if err != nil { if err == context.DeadlineExceeded { err = &errortypes.Timeout{Message: err.Error()} + if tb, ok := bidder.Bidder.(adapters.TimeoutBidder); ok { + // Toss the timeout notification call into a go routine, as we are out of time' + // and cannot delay processing. We don't do anything result, as there is not much + // we can do about a timeout notification failure. We do not want to get stuck in + // a loop of trying to report timeouts to the timeout notifications. + go bidder.doTimeoutNotification(tb, req) + } + } return &httpCallInfo{ request: req, @@ -328,6 +337,21 @@ func (bidder *bidderAdapter) doRequest(ctx context.Context, req *adapters.Reques } } +func (bidder *bidderAdapter) doTimeoutNotification(timeoutBidder adapters.TimeoutBidder, req *adapters.RequestData) { + ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) + defer cancel() + toReq, errL := timeoutBidder.MakeTimeoutNotification(req) + if toReq != nil && len(errL) == 0 { + httpReq, err := http.NewRequest(toReq.Method, toReq.Uri, bytes.NewBuffer(toReq.Body)) + if err == nil { + httpReq.Header = req.Headers + ctxhttp.Do(ctx, bidder.Client, httpReq) + // No validation yet on sending notifications + } + } + +} + type httpCallInfo struct { request *adapters.RequestData response *adapters.ResponseData