Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
224 changes: 224 additions & 0 deletions adapters/aniview/aniview.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
package aniview

import (
"bytes"
"fmt"
"net/http"
"strings"

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

type adapter struct {
endpoint string
}

type aniviewExt struct {
PBS int `json:"pbs"`
}

// Builder builds a new instance of the Aniview adapter for the given bidder with the given config.
func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
bidder := &adapter{
endpoint: config.Endpoint,
}
return bidder, nil
}

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

headers := http.Header{}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

http.Header is a map[string][]string — a reference type. Creating it once outside the loop and assigning it to every RequestData.Headers means all outgoing requests share the same map. If PBS or any middleware mutates one request's headers (e.g. adds a correlation ID), it silently modifies every other request in the batch.

Please create a fresh http.Header{} per RequestData, inside the inner loop:

headers := http.Header{}
headers.Add("Content-Type", "application/json;charset=utf-8")
headers.Add("Accept", "application/json")
requests = append(requests, &adapters.RequestData{
    ...
    Headers: headers,
})

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done

headers.Add("Content-Type", "application/json;charset=utf-8")
headers.Add("Accept", "application/json")

requestExt, err := buildRequestExt(request.Ext)
if err != nil {
return nil, []error{err}
}

// One outgoing request per imp and per media type, mirroring the Prebid.js adapter.
for _, imp := range request.Imp {
impExt, err := extractImpExt(&imp)
if err != nil {
errors = append(errors, err)
continue
}

imp.TagID = strings.TrimSpace(impExt.ChannelId)

for _, singleTypeImp := range splitImpByMediaType(imp) {
requestCopy := *request
requestCopy.Imp = []openrtb2.Imp{singleTypeImp}
requestCopy.Ext = requestExt

requestJSON, err := jsonutil.Marshal(&requestCopy)
if err != nil {
errors = append(errors, fmt.Errorf("marshal bidRequest: %w", err))
continue
}

requests = append(requests, &adapters.RequestData{
Method: "POST",
Uri: a.endpoint,
Body: requestJSON,
Headers: headers,
ImpIDs: []string{singleTypeImp.ID},
})
}
}

return requests, errors
}

// buildRequestExt merges ext.aniview into the existing request.ext, preserving
// whatever PBS core has put there.
func buildRequestExt(requestExt []byte) ([]byte, error) {
extMap := map[string]interface{}{}
if len(requestExt) > 0 {
if err := jsonutil.Unmarshal(requestExt, &extMap); err != nil {
return nil, fmt.Errorf("unmarshal request.ext: %w", err)
}
}

extMap["aniview"] = aniviewExt{PBS: 1}

ext, err := jsonutil.Marshal(extMap)
if err != nil {
return nil, fmt.Errorf("marshal request.ext: %w", err)
}
return ext, nil
}

// splitImpByMediaType returns one imp per media type, so each outgoing request
// carries a single media type, as the Aniview endpoint expects.
func splitImpByMediaType(imp openrtb2.Imp) []openrtb2.Imp {
if imp.Video == nil || imp.Banner == nil {
return []openrtb2.Imp{imp}
}

videoImp := imp
videoImp.Banner = nil
bannerImp := imp
bannerImp.Video = nil
return []openrtb2.Imp{videoImp, bannerImp}
}

func extractImpExt(imp *openrtb2.Imp) (*openrtb_ext.ImpExtAniview, error) {
var bidderExt adapters.ExtImpBidder
if err := jsonutil.Unmarshal(imp.Ext, &bidderExt); err != nil {
return nil, fmt.Errorf("unmarshal bidderExt: %w", err)
}

var impExt openrtb_ext.ImpExtAniview
if err := jsonutil.Unmarshal(bidderExt.Bidder, &impExt); err != nil {
return nil, fmt.Errorf("unmarshal ImpExtAniview: %w", err)
}

if strings.TrimSpace(impExt.ChannelId) == "" {
return nil, &errortypes.BadInput{
Message: fmt.Sprintf("Missing AV_CHANNELID for imp: %s", imp.ID),
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

adapters.CheckResponseStatusCodeForErrors already returns the right typed error — BadInput for 400 and BadServerResponse for everything else (500, 503, etc.). The current code discards that typed error and always wraps a new errortypes.BadInput, so a 500 response from the exchange is misclassified as a buyer error.

Please just propagate the helper's return value:
if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil {
return nil, []error{err}
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done

return &impExt, nil
}

func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) {
var errs []error

if adapters.IsResponseStatusCodeNoContent(responseData) {
return nil, nil
}

if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil {
return nil, []error{&errortypes.BadInput{
Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", responseData.StatusCode),
}}
}

// The exchange may answer a no-bid as 200 with an empty/whitespace body.
if len(bytes.TrimSpace(responseData.Body)) == 0 {
return nil, nil
}

var response openrtb2.BidResponse
if err := jsonutil.Unmarshal(responseData.Body, &response); err != nil {
return nil, []error{&errortypes.BadServerResponse{
Message: fmt.Sprintf("bad server response: %s", err),
}}
}

// The outgoing request is single media type per imp — use it for media type
// inference, not the original request (a multi-format imp carries both types there).
var sentRequest openrtb2.BidRequest
if err := jsonutil.Unmarshal(requestData.Body, &sentRequest); err != nil {
return nil, []error{fmt.Errorf("unmarshal outgoing request: %w", err)}
}

bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(response.SeatBid))

if response.Cur != "" {
bidResponse.Currency = response.Cur
}

for _, seatBid := range response.SeatBid {
for i, bid := range seatBid.Bid {
// Mirror the Prebid.js adapter: a bid without markup or a VAST url is unusable.
if bid.AdM == "" && bid.NURL == "" {
continue
}

bidType, err := getMediaTypeForBid(&bid, &sentRequest)
if err != nil {
errs = append(errs, err)
continue
}

bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
Bid: &seatBid.Bid[i],
BidType: bidType,
})
}
}

return bidResponse, errs
}

// getMediaTypeForBid resolves the bid media type: bid.mtype when provided,
// otherwise the media type of the matching imp (requests are single media type),
// otherwise VAST markup detection.
func getMediaTypeForBid(bid *openrtb2.Bid, request *openrtb2.BidRequest) (openrtb_ext.BidType, error) {
switch bid.MType {
case openrtb2.MarkupBanner:
return openrtb_ext.BidTypeBanner, nil
case openrtb2.MarkupVideo:
return openrtb_ext.BidTypeVideo, nil
}

for _, imp := range request.Imp {
if imp.ID == bid.ImpID {
if imp.Video != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Consider this as a suggestion. The current implementation follows an anti-pattern, assumes that if there is a multi-format request, the media type defaults to openrtb_ext.BidTypeVideo, nil. Prebid server expects the media type to be explicitly set in the adapter response. Therefore, we strongly recommend implementing a pattern where the adapter server sets the MType field in the response to accurately determine the media type for the impression.

return openrtb_ext.BidTypeVideo, nil
}
if imp.Banner != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Consider this as a suggestion. The current implementation follows an anti-pattern, assumes that if there is a multi-format request, the media type defaults to openrtb_ext.BidTypeBanner, nil. Prebid server expects the media type to be explicitly set in the adapter response. Therefore, we strongly recommend implementing a pattern where the adapter server sets the MType field in the response to accurately determine the media type for the impression.

return openrtb_ext.BidTypeBanner, nil
}
break
}
}

adm := strings.TrimSpace(strings.ToLower(bid.AdM))
if strings.HasPrefix(adm, "<vast") || strings.HasPrefix(adm, "<?xml") {
return openrtb_ext.BidTypeVideo, nil
}

return "", &errortypes.BadServerResponse{
Message: fmt.Sprintf("Could not define bid type for imp: %s", bid.ImpID),
}
}
48 changes: 48 additions & 0 deletions adapters/aniview/aniview_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package aniview

import (
"testing"

"github.com/prebid/prebid-server/v4/adapters"
"github.com/prebid/prebid-server/v4/adapters/adapterstest"
"github.com/prebid/prebid-server/v4/config"
"github.com/prebid/prebid-server/v4/openrtb_ext"
)

func TestBuildRequestExtInvalid(t *testing.T) {
if _, err := buildRequestExt([]byte(`"not-an-object"`)); err == nil {
t.Error("expected error for non-object request.ext")
}
ext, err := buildRequestExt([]byte(`{"prebid":{"debug":true}}`))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(ext) == "" {
t.Error("expected merged ext")
}
}

func TestMakeBidsEmptyBody(t *testing.T) {
bidder, _ := Builder(openrtb_ext.BidderAniview, config.Adapter{Endpoint: "https://rtb.aniview.com/sspRTB2"}, config.Server{})
for _, body := range []string{"", "\n", " \n"} {
resp, errs := bidder.(*adapter).MakeBids(nil, &adapters.RequestData{Body: []byte("{}")}, &adapters.ResponseData{StatusCode: 200, Body: []byte(body)})
if resp != nil || errs != nil {
t.Errorf("empty body %q should be a silent no-bid, got resp=%v errs=%v", body, resp, errs)
}
}
}

func TestJsonSamples(t *testing.T) {
bidder, buildErr := Builder(openrtb_ext.BidderAniview, config.Adapter{
Endpoint: "https://rtb.aniview.com/sspRTB2",
},
config.Server{
ExternalUrl: "http://hosturl.com", GvlID: 780, DataCenter: "2",
})

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

adapterstest.RunJSONBidderTest(t, "aniviewtest", bidder)
}
104 changes: 104 additions & 0 deletions adapters/aniview/aniviewtest/exemplary/banner.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
{
"mockBidRequest": {
"id": "banner-request-id",
"site": {
"page": "https://publisher.com/page"
},
"imp": [
{
"id": "banner-imp-id",
"banner": {
"w": 300,
"h": 250,
"format": [{"w": 300, "h": 250}]
},
"ext": {
"bidder": {
"AV_PUBLISHERID": "1234567890abcdef12345678",
"AV_CHANNELID": "abcdef1234567890abcdef12"
}
}
}
]
},
"httpCalls": [
{
"expectedRequest": {
"uri": "https://rtb.aniview.com/sspRTB2",
"body": {
"id": "banner-request-id",
"site": {
"page": "https://publisher.com/page"
},
"imp": [
{
"id": "banner-imp-id",
"tagid": "abcdef1234567890abcdef12",
"banner": {
"w": 300,
"h": 250,
"format": [{"w": 300, "h": 250}]
},
"ext": {
"bidder": {
"AV_PUBLISHERID": "1234567890abcdef12345678",
"AV_CHANNELID": "abcdef1234567890abcdef12"
}
}
}
],
"ext": {
"aniview": {
"pbs": 1
}
}
},
"impIDs": ["banner-imp-id"]
},
"mockResponse": {
"status": 200,
"body": {
"id": "banner-request-id",
"seatbid": [
{
"seat": "1234567890abcdef12345678",
"bid": [
{
"id": "banner-bid-id",
"impid": "banner-imp-id",
"price": 1.2,
"adm": "<div id=\"aniview-ad\"><script src=\"https://cdn.aniview.com/tag.js\"></script></div>",
"crid": "banner-creative-id",
"adomain": ["advertiser.com"],
"w": 300,
"h": 250
}
]
}
]
}
}
}
],
"expectedBidResponses": [
{
"currency": "USD",
"bids": [
{
"bid": {
"id": "banner-bid-id",
"impid": "banner-imp-id",
"price": 1.2,
"adm": "<div id=\"aniview-ad\"><script src=\"https://cdn.aniview.com/tag.js\"></script></div>",
"crid": "banner-creative-id",
"adomain": ["advertiser.com"],
"w": 300,
"h": 250
},
"type": "banner"
}
]
}
],
"expectedMakeBidsErrors": []
}
Loading
Loading