Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
277 changes: 277 additions & 0 deletions adapters/clydo/clydo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
package clydo

import (
"fmt"
"net/http"
"text/template"

"github.com/prebid/openrtb/v20/openrtb2"
"github.com/prebid/prebid-server/v3/adapters"
"github.com/prebid/prebid-server/v3/config"
"github.com/prebid/prebid-server/v3/errortypes"
"github.com/prebid/prebid-server/v3/macros"
"github.com/prebid/prebid-server/v3/openrtb_ext"
"github.com/prebid/prebid-server/v3/util/jsonutil"
)

type adapter struct {
endpoint *template.Template
}

func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
template, err := template.New("endpointTemplate").Parse(config.Endpoint)
if err != nil {
return nil, fmt.Errorf("Failed to parse endpoint url: %v", err)
}
bidder := &adapter{
endpoint: template,
}
return bidder, nil
}

func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
var requests []*adapters.RequestData
var errors []error

for _, imp := range request.Imp {
reqData, err := a.prepareRequest(request, imp)
if err != nil {
errors = append(errors, err)
continue
}
requests = append(requests, reqData)
}
return requests, errors
}

func (a *adapter) MakeBids(
request *openrtb2.BidRequest,
requestData *adapters.RequestData,
responseData *adapters.ResponseData,
) (*adapters.BidderResponse, []error) {
if errResp := checkResponseStatus(responseData); errResp != nil {
return nil, errResp
Copy link
Contributor

@guscarreon guscarreon Oct 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm ok with the checkResponseStatus(responseData) function as it's not too big nor hard to maintain. However, most of the logic in checkResponseStatus(responseData) is already in the adapter framework-wide functions adapters.CheckResponseStatusCodeForErrors(responseData) and adapters.IsResponseStatusCodeNoContent(responseData) found in adapters/response.go. I think it'd be better to use those functions in order to not maintain more lines of code than we need to, but I leave it up to you.

Let me know your thoughts

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sense

}
response, err := prepareBidResponse(responseData.Body)
if err != nil {
return nil, []error{err}
}

bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp))
if response.Cur != "" {
bidResponse.Currency = response.Cur
}

bidTypeMap, err := buildBidTypeMap(request.Imp)
if err != nil {
return nil, []error{err}
}
bids, errors := prepareSeatBids(response.SeatBid, bidTypeMap)
bidResponse.Bids = bids

return bidResponse, errors
}

func (a *adapter) prepareRequest(request *openrtb2.BidRequest, imp openrtb2.Imp) (*adapters.RequestData, error) {
params, err := prepareExtParams(imp)
if err != nil {
return nil, err
}
endpoint, err := a.prepareEndpoint(params)
if err != nil {
return nil, err
}
body, err := prepareBody(request, imp)
if err != nil {
return nil, err
}
headers, err := prepareHeaders(request)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inside prepareRequest(request, imp), the outcome of prepareHeaders(request) doesn't seem to depend on the particular imp.Ext in question. Does prepareHeaders(request) need to be called more than once? If not, can it, similarly to prepareImpIds(request), get moved outside the for loop in line 36?

 32   func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
 33       var requests []*adapters.RequestData
 34       var errors []error
 35  
    +     headers, err := prepareHeaders(request)
    +     if err != nil {
    +         return nil, append(errors, err)
    +     }
    +     
 36       for _, imp := range request.Imp {
 37 -         reqData, err := a.prepareRequest(request, imp)
    +         reqData, err := a.prepareRequest(request, imp, headers)
 38           if err != nil {
 39               errors = append(errors, err)
 40               continue
 41           }
 42           requests = append(requests, reqData)
 43       }
 44       return requests, errors
 45   }
 46  
 47 *-- 27 lines: func (a *adapter) MakeBids(-------------------------------------------------------------------------------------------------
 74  
 75 - func (a *adapter) prepareRequest(request *openrtb2.BidRequest, imp openrtb2.Imp) (*adapters.RequestData, error) {
    + func (a *adapter) prepareRequest(request *openrtb2.BidRequest, imp openrtb2.Imp, headers http.Header) (*adapters.RequestData, error) {
 76       params, err := prepareExtParams(imp)
 77       if err != nil {
 78           return nil, err
 79       }
 80 *--  7 lines: endpoint, err := a.prepareEndpoint(params)----------------------------------------------------------------------------------
 87       }
 88 -     headers, err := prepareHeaders(request)
 89 -     if err != nil {
 90 -         return nil, err
 91 -     }
 92  
 93       impIds, err := prepareImpIds(request)
 94       if err != nil {
 95           return nil, err
 96       }
 97  
 98       return &adapters.RequestData{
 99           Method:  "POST",
100           Uri:     endpoint,
101           Body:    body,
102           Headers: headers,
103           ImpIDs:  impIds,
104       }, nil
105   }

if err != nil {
return nil, err
}

impIds, err := prepareImpIds(request)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is prepareImpIds() supposed to be called with a request that only has one imp in its Imp[] array as opposed to the original request? If not, if it's meant to be called as is in the current commit, with the original request as parameter, including all its original Imp[] elements: should the prepareImpIds() call get moved ouside the for loop in line 36?

Function openrtb_ext.GetImpIDs(request.Imp), called from prepareImpIds(request), traverses the n number of elements of request.Imp[] to build the impIds list and, given that prepareRequest(request, imp) gets called per every element of request.Imp[] in line 37 inside the for loop, impIds gets built multiple times. It gets built n times to be exact. Therefore, if prepareImpIds() is supposed to be passed the original request, and not a single imp request, we can optimize calling prepareImpIds(request) gets just once, outside the for loop.

 32   func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
 33       var requests []*adapters.RequestData
 34       var errors []error
 35  
    +     impIds, err := prepareImpIds(request)
    +     if err != nil {
    +         return nil, append(errors, err)
    +     }
    +
 36       for _, imp := range request.Imp {
 37 -         reqData, err := a.prepareRequest(request, imp)
    +         reqData, err := a.prepareRequest(request, imp, impIds)
 38           if err != nil {
 39               errors = append(errors, err)
 40               continue
 41           }
 42           requests = append(requests, reqData)
 43       }
 44       return requests, errors
 45   }
 46  
 47 *-- 27 lines: func (a *adapter) MakeBids(--------------------------------------------------------------------------------
 74  
 75 - func (a *adapter) prepareRequest(request *openrtb2.BidRequest, imp openrtb2.Imp) (*adapters.RequestData, error) {
    + func (a *adapter) prepareRequest(request *openrtb2.BidRequest, imp openrtb2.Imp, impIds []string) (*adapters.RequestData, error) {
 76       params, err := prepareExtParams(imp)
 77 *-- 15 lines: if err != nil {--------------------------------------------------------------------------------------------
 92  
 93 -     impIds, err := prepareImpIds(request)
 94 -     if err != nil {
 95 -         return nil, err
 96 -     }
 97  
 98       return &adapters.RequestData{
 99           Method:  "POST",
100           Uri:     endpoint,
101           Body:    body,
102           Headers: headers,
103           ImpIDs:  impIds,
104       }, nil
105   }
adapters/clydo/clydo.go

Thoughts?

if err != nil {
return nil, err
}

return &adapters.RequestData{
Method: "POST",
Uri: endpoint,
Body: body,
Headers: headers,
ImpIDs: impIds,
}, nil
}

func prepareExtParams(imp openrtb2.Imp) (*openrtb_ext.ImpExtClydo, error) {
var clydoImpExt openrtb_ext.ImpExtClydo
var bidderExt adapters.ExtImpBidder
if err := jsonutil.Unmarshal(imp.Ext, &bidderExt); err != nil {
return nil, &errortypes.BadInput{
Message: "missing ext.bidder",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An unmarshalling error doesn’t always indicate a missing field — it could also result from formatting issues or incorrect data types. We should check for an empty imp.ext and handle such errors gracefully.

}
}
if err := jsonutil.Unmarshal(bidderExt.Bidder, &clydoImpExt); err != nil {
return nil, &errortypes.BadInput{
Message: "invalid ext.bidder",
}
}
return &clydoImpExt, nil
}

func (a *adapter) prepareEndpoint(params *openrtb_ext.ImpExtClydo) (string, error) {
partnerId := params.PartnerId
if partnerId == "" {
return "", &errortypes.BadInput{
Message: "invalid partnerId",
}
}

region := params.Region
if region == "" {
region = "us"
}

endpointParams := macros.EndpointTemplateParams{
PartnerId: partnerId,
Region: region,
}
return macros.ResolveMacros(a.endpoint, endpointParams)
}

func prepareBody(request *openrtb2.BidRequest, imp openrtb2.Imp) ([]byte, error) {
reqCopy := *request
reqCopy.Imp = []openrtb2.Imp{imp}

reqCopy.Imp[0].Ext = imp.Ext
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for this assignment because all of the imp fields are already in reqCopy.Imp[0]


body, err := jsonutil.Marshal(&reqCopy)
if err != nil {
return nil, err
}

return body, nil
}

func prepareHeaders(request *openrtb2.BidRequest) (http.Header, error) {
allHeaders := map[string]string{
"X-OpenRTB-Version": "2.5",
"Accept": "application/json",
"Content-Type": "application/json; charset=utf-8",
}

allHeaders, err := appendDeviceHeaders(allHeaders, request)
if err != nil {
return nil, err
}

headers := make(http.Header)
for k, v := range allHeaders {
headers.Add(k, v)
}

return headers, nil
}

func appendDeviceHeaders(headers map[string]string, request *openrtb2.BidRequest) (map[string]string, error) {
if request.Device == nil {
return nil, &errortypes.BadInput{Message: "Failed to get device headers"}
}

if ipv6 := request.Device.IPv6; ipv6 != "" {
headers["X-Forwarded-For"] = ipv6
}
if ip := request.Device.IP; ip != "" {
headers["X-Forwarded-For"] = ip
}
if ua := request.Device.UA; ua != "" {
headers["User-Agent"] = ua
}

return headers, nil
}

func prepareImpIds(request *openrtb2.BidRequest) ([]string, error) {
impIds := openrtb_ext.GetImpIDs(request.Imp)
if impIds == nil {
return nil, &errortypes.BadInput{Message: "Failed to get imp ids"}
}
return impIds, nil
}

func checkResponseStatus(responseData *adapters.ResponseData) []error {
switch responseData.StatusCode {
case http.StatusNoContent:
return []error{}
case http.StatusBadRequest:
return []error{&errortypes.BadInput{
Message: "Bad request. Run with request.debug = 1 for more info.",
}}
case http.StatusOK:
return nil
default:
return []error{&errortypes.BadServerResponse{
Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info.", responseData.StatusCode),
}}
}
}

func prepareBidResponse(body []byte) (openrtb2.BidResponse, error) {
var response openrtb2.BidResponse
if err := jsonutil.Unmarshal(body, &response); err != nil {
return response, err
}
return response, nil
}

func prepareSeatBids(seatBids []openrtb2.SeatBid, bidTypeMap map[string]openrtb_ext.BidType) ([]*adapters.TypedBid, []error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: In its current form, under no scenario prepareSeatBids() returns an error nor an array or errors. Should we remove and simplify the function signature?

223
224 - func prepareSeatBids(seatBids []openrtb2.SeatBid, bidTypeMap map[string]openrtb_ext.BidType) ([]*adapters.TypedBid, []error) {
    + func prepareSeatBids(seatBids []openrtb2.SeatBid, bidTypeMap map[string]openrtb_ext.BidType) []*adapters.TypedBid {
225       var typedBids []*adapters.TypedBid
226 -     var errors []error
227  
228       if seatBids == nil {
adapters/clydo/clydo.go

var typedBids []*adapters.TypedBid
var errors []error

if seatBids == nil {
return typedBids, nil
}

for _, seatBid := range seatBids {
if seatBid.Bid == nil {
continue
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: no need for the checks as the range keyword takes care of the nil value scenario

224   func prepareSeatBids(seatBids []openrtb2.SeatBid, bidTypeMap map[string]openrtb_ext.BidType) ([]*adapters.TypedBid, []error) {
225       var typedBids []*adapters.TypedBid
226       var errors []error
227  
228 -     if seatBids == nil {
229 -         return typedBids, nil
230 -     }
231  
232       for _, seatBid := range seatBids {
233 -         if seatBid.Bid == nil {
234 -             continue
235 -         }
236           for i := range seatBid.Bid {
237               bid := &seatBid.Bid[i]
238               bidType := getMediaTypeForBid(bid, bidTypeMap)
adapters/clydo/clydo.go

for i := range seatBid.Bid {
bid := &seatBid.Bid[i]
bidType := getMediaTypeForBid(bid, bidTypeMap)
typedBids = append(typedBids, &adapters.TypedBid{
Bid: bid,
BidType: bidType,
})
}
}

return typedBids, errors
}

func buildBidTypeMap(imps []openrtb2.Imp) (map[string]openrtb_ext.BidType, error) {
bidTypeMap := make(map[string]openrtb_ext.BidType, len(imps))
for _, imp := range imps {
switch {
case imp.Video != nil:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use metadata from the bid to determine the media type instead of relying on the impression? Relying solely on the impression could cause issues in multi-format requests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clydo does not support multi-format requests, so let's stick with impression-based mapping

bidTypeMap[imp.ID] = openrtb_ext.BidTypeVideo
case imp.Native != nil:
bidTypeMap[imp.ID] = openrtb_ext.BidTypeNative
case imp.Banner != nil:
bidTypeMap[imp.ID] = openrtb_ext.BidTypeBanner
default:
return nil, &errortypes.BadInput{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your static/bidder-info/clydo.yaml file lists audio as supported media type but, if a request with an imp.Audio was to come, your buildBidTypeMap(imps []openrtb2.Imp) function would throw an errortypes.BadInput error in line 266. Should audio be removed from the list? OR should it be allowed in buildBidTypeMap(imps []openrtb2.Imp)?

14 capabilities:
15   app:
16     mediaTypes:
17       - banner
18       - video
19       - audio
20       - native
21   site:
22     mediaTypes:
23       - banner
24       - video
25       - audio
26       - native
static/bidder-info/clydo.yaml

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added audio to buildBidTypeMap, clydo can support it

Message: "Failed to get media type",
}
}
}
return bidTypeMap, nil
}

func getMediaTypeForBid(bid *openrtb2.Bid, bidTypeMap map[string]openrtb_ext.BidType) openrtb_ext.BidType {
if mediaType, ok := bidTypeMap[bid.ImpID]; ok {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are imp.ID collisions likely? In other words, is an edge case like the one shown below, where two imp.IDs are the same for two imp elements with different formats, feasible?

req.Imp = []openrtb2.Imp{
    {ID: "sameID", Video: &openrtb2.Video{ /* not-nil */ },
    {ID: "sameID", Banner: &openrtb2.Banner{ /* not-nil */ },
    {ID: "anotherID", Native: &openrtb2.Native{ /* not-nil */ },
}

If this edge case ever presents itself, it seems that the the current logic would overwrite the "sameID" entry in bidTypeMap. bidTypeMap would look like:

bidTypeMap := map[string]openrtb_ext.BidType{
    "sameID":    openrtb_ext.BidTypeBanner,
    "anotherID": openrtb_ext.BidTypeNative,
}

Which would mislabel one bid as a banner, when in reality it is a video. Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, it's better to validate duplicates

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This scenario can occur in the case of a multi-format request. Does Clydo support multi-format requests? If so, would it be better to determine the media type from the bid instead of relying on the impression?

return mediaType
}
return openrtb_ext.BidTypeBanner
}
30 changes: 30 additions & 0 deletions adapters/clydo/clydo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package clydo

import (
"testing"

"github.com/prebid/prebid-server/v3/adapters/adapterstest"
"github.com/prebid/prebid-server/v3/config"
"github.com/prebid/prebid-server/v3/openrtb_ext"
"github.com/stretchr/testify/assert"
)

func TestJsonSamples(t *testing.T) {
bidder, buildErr := Builder(openrtb_ext.BidderClydo, config.Adapter{
Endpoint: "http://{{.Region}}.clydo.io/{{.PartnerId}}"},
config.Server{ExternalUrl: "http://hosturl.com"})

if buildErr != nil {
t.Fatalf("Builder returned unexpected error %v", buildErr)
}

adapterstest.RunJSONBidderTest(t, "clydotest", bidder)
}

func TestEndpointTemplateMalformed(t *testing.T) {
_, buildErr := Builder(openrtb_ext.BidderClydo, config.Adapter{
Endpoint: "{{Malformed}}"},
config.Server{ExternalUrl: "http://hosturl.com"})

assert.Error(t, buildErr)
}
Loading
Loading