Skip to content

Commit

Permalink
Implement x-prebid headers support (#1301)
Browse files Browse the repository at this point in the history
  • Loading branch information
SerhiiNahornyi authored Jun 11, 2021
1 parent 4710f5d commit ba338a6
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 116 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package org.prebid.server.bidder;

import com.iab.openrtb.request.App;
import com.iab.openrtb.request.BidRequest;
import io.vertx.core.MultiMap;
import org.apache.commons.lang3.StringUtils;
import org.prebid.server.proto.openrtb.ext.request.ExtApp;
import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid;
import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel;
import org.prebid.server.util.HttpUtil;

import java.util.Collections;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class HttpBidderRequestEnricher {

private static final Set<CharSequence> HEADERS_TO_COPY = Collections.singleton(HttpUtil.SEC_GPC_HEADER.toString());

private final String pbsRecord;

public HttpBidderRequestEnricher(String pbsVersion) {
pbsRecord = createNameVersionRecord("pbs-java", Objects.requireNonNull(pbsVersion));
}

MultiMap enrichHeaders(MultiMap bidderRequestHeaders, MultiMap originalRequestHeaders, BidRequest bidRequest) {
// some bidders has headers on class level, so we create copy to not affect them
final MultiMap bidderRequestHeadersCopy = copyMultiMap(bidderRequestHeaders);

addOriginalRequestHeaders(originalRequestHeaders, bidderRequestHeadersCopy);
addXPrebidHeader(bidderRequestHeadersCopy, bidRequest);

return bidderRequestHeadersCopy;
}

private static MultiMap copyMultiMap(MultiMap source) {
final MultiMap copiedMultiMap = MultiMap.caseInsensitiveMultiMap();
if (source != null && !source.isEmpty()) {
source.forEach(entry -> copiedMultiMap.add(entry.getKey(), entry.getValue()));
}
return copiedMultiMap;
}

private static void addOriginalRequestHeaders(MultiMap originalHeaders, MultiMap bidderHeaders) {
originalHeaders.entries().stream()
.filter(entry -> HEADERS_TO_COPY.contains(entry.getKey())
&& !bidderHeaders.contains(entry.getKey()))
.forEach(entry -> bidderHeaders.add(entry.getKey(), entry.getValue()));
}

private void addXPrebidHeader(MultiMap headers, BidRequest bidRequest) {
final String channelRecord = resolveChannelVersionRecord(bidRequest.getExt());
final String sdkRecord = resolveSdkVersionRecord(bidRequest.getApp());
final String value = Stream.of(channelRecord, sdkRecord, pbsRecord)
.filter(Objects::nonNull)
.collect(Collectors.joining(","));
HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_PREBID_HEADER, value);
}

private static String resolveChannelVersionRecord(ExtRequest extRequest) {
final ExtRequestPrebid extPrebid = extRequest != null ? extRequest.getPrebid() : null;
final ExtRequestPrebidChannel channel = extPrebid != null ? extPrebid.getChannel() : null;
final String channelName = channel != null ? channel.getName() : null;
final String channelVersion = channel != null ? channel.getVersion() : null;

return createNameVersionRecord(channelName, channelVersion);
}

private static String resolveSdkVersionRecord(App app) {
final ExtApp extApp = app != null ? app.getExt() : null;
final ExtAppPrebid extPrebid = extApp != null ? extApp.getPrebid() : null;
final String sdkSource = extPrebid != null ? extPrebid.getSource() : null;
final String sdkVersion = extPrebid != null ? extPrebid.getVersion() : null;

return createNameVersionRecord(sdkSource, sdkVersion);
}

private static String createNameVersionRecord(String name, String version) {
return StringUtils.isNotEmpty(name) && StringUtils.isNotEmpty(version)
? String.format("%s/%s", name, version)
: null;
}
}
41 changes: 11 additions & 30 deletions src/main/java/org/prebid/server/bidder/HttpBidderRequester.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import io.netty.handler.codec.http.HttpResponseStatus;
import io.vertx.core.CompositeFuture;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import io.vertx.ext.web.RoutingContext;
Expand All @@ -22,7 +21,6 @@
import org.prebid.server.bidder.model.Result;
import org.prebid.server.execution.Timeout;
import org.prebid.server.proto.openrtb.ext.response.ExtHttpCall;
import org.prebid.server.util.HttpUtil;
import org.prebid.server.vertx.http.HttpClient;
import org.prebid.server.vertx.http.model.HttpClientResponse;

Expand All @@ -32,7 +30,6 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand All @@ -50,19 +47,20 @@ public class HttpBidderRequester {

private static final Logger logger = LoggerFactory.getLogger(HttpBidderRequester.class);

private static final Set<CharSequence> HEADERS_TO_COPY = Collections.singleton(HttpUtil.SEC_GPC.toString());

private final HttpClient httpClient;
private final BidderRequestCompletionTrackerFactory completionTrackerFactory;
private final BidderErrorNotifier bidderErrorNotifier;
private final HttpBidderRequestEnricher requestEnricher;

public HttpBidderRequester(HttpClient httpClient,
BidderRequestCompletionTrackerFactory completionTrackerFactory,
BidderErrorNotifier bidderErrorNotifier) {
BidderErrorNotifier bidderErrorNotifier,
HttpBidderRequestEnricher requestEnricher) {

this.httpClient = Objects.requireNonNull(httpClient);
this.completionTrackerFactory = completionTrackerFactoryOrFallback(completionTrackerFactory);
this.bidderErrorNotifier = Objects.requireNonNull(bidderErrorNotifier);
this.requestEnricher = Objects.requireNonNull(requestEnricher);
}

/**
Expand All @@ -77,7 +75,8 @@ public <T> Future<BidderSeatBid> requestBids(Bidder<T> bidder,

final Result<List<HttpRequest<T>>> httpRequestsWithErrors = bidder.makeHttpRequests(bidRequest);
final List<BidderError> bidderErrors = httpRequestsWithErrors.getErrors();
final List<HttpRequest<T>> httpRequests = enrichRequests(httpRequestsWithErrors.getValue(), routingContext);
final List<HttpRequest<T>> httpRequests =
enrichRequests(httpRequestsWithErrors.getValue(), routingContext, bidRequest);

if (CollectionUtils.isEmpty(httpRequests)) {
return emptyBidderSeatBidWithErrors(bidderErrors);
Expand Down Expand Up @@ -108,34 +107,16 @@ public <T> Future<BidderSeatBid> requestBids(Bidder<T> bidder,
.map(ignored -> resultBuilder.toBidderSeatBid(debugEnabled));
}

private static <T> List<HttpRequest<T>> enrichRequests(List<HttpRequest<T>> httpRequests,
RoutingContext routingContext) {

private <T> List<HttpRequest<T>> enrichRequests(List<HttpRequest<T>> httpRequests,
RoutingContext routingContext,
BidRequest bidRequest) {
return httpRequests.stream().map(httpRequest -> httpRequest.toBuilder()
.headers(enrichHeaders(httpRequest.getHeaders(), routingContext.request().headers()))
.headers(requestEnricher.enrichHeaders(httpRequest.getHeaders(),
routingContext.request().headers(), bidRequest))
.build())
.collect(Collectors.toList());
}

private static MultiMap enrichHeaders(MultiMap bidderRequestHeaders, MultiMap originalRequestHeaders) {
// some bidders has headers on class level, so we create copy to not affect them
final MultiMap bidderRequestHeadersCopy = copyMultiMap(bidderRequestHeaders);

originalRequestHeaders.entries().stream()
.filter(entry -> HEADERS_TO_COPY.contains(entry.getKey())
&& !bidderRequestHeadersCopy.contains(entry.getKey()))
.forEach(entry -> bidderRequestHeadersCopy.add(entry.getKey(), entry.getValue()));
return bidderRequestHeadersCopy;
}

private static MultiMap copyMultiMap(MultiMap source) {
final MultiMap copiedMultiMap = MultiMap.caseInsensitiveMultiMap();
if (source != null && !source.isEmpty()) {
source.forEach(entry -> copiedMultiMap.add(entry.getKey(), entry.getValue()));
}
return copiedMultiMap;
}

private <T> boolean isStoredResponse(List<HttpRequest<T>> httpRequests, String storedResponse, String bidder) {
if (StringUtils.isBlank(storedResponse)) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.prebid.server.bidder.BidderDeps;
import org.prebid.server.bidder.BidderErrorNotifier;
import org.prebid.server.bidder.BidderRequestCompletionTrackerFactory;
import org.prebid.server.bidder.HttpBidderRequestEnricher;
import org.prebid.server.bidder.HttpBidderRequester;
import org.prebid.server.cache.CacheService;
import org.prebid.server.cache.model.CacheTtl;
Expand Down Expand Up @@ -462,9 +463,19 @@ BidderCatalog bidderCatalog(List<BidderDeps> bidderDeps) {
HttpBidderRequester httpBidderRequester(
HttpClient httpClient,
@Autowired(required = false) BidderRequestCompletionTrackerFactory bidderRequestCompletionTrackerFactory,
BidderErrorNotifier bidderErrorNotifier) {
BidderErrorNotifier bidderErrorNotifier,
HttpBidderRequestEnricher requestEnricher) {

return new HttpBidderRequester(httpClient, bidderRequestCompletionTrackerFactory, bidderErrorNotifier);
return new HttpBidderRequester(httpClient,
bidderRequestCompletionTrackerFactory,
bidderErrorNotifier,
requestEnricher);
}

@Bean
HttpBidderRequestEnricher httpBidderRequestEnricher(VersionInfo versionInfo) {

return new HttpBidderRequestEnricher(versionInfo.getVersion());
}

@Bean
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/org/prebid/server/util/HttpUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public final class HttpUtil {
public static final CharSequence X_REQUEST_AGENT_HEADER = HttpHeaders.createOptimized("X-Request-Agent");
public static final CharSequence ORIGIN_HEADER = HttpHeaders.createOptimized("Origin");
public static final CharSequence ACCEPT_HEADER = HttpHeaders.createOptimized("Accept");
public static final CharSequence SEC_GPC = HttpHeaders.createOptimized("Sec-GPC");
public static final CharSequence SEC_GPC_HEADER = HttpHeaders.createOptimized("Sec-GPC");
public static final CharSequence CONTENT_TYPE_HEADER = HttpHeaders.createOptimized("Content-Type");
public static final CharSequence X_REQUESTED_WITH_HEADER = HttpHeaders.createOptimized("X-Requested-With");
public static final CharSequence REFERER_HEADER = HttpHeaders.createOptimized("Referer");
Expand All @@ -49,6 +49,7 @@ public final class HttpUtil {
public static final CharSequence CONNECTION_HEADER = HttpHeaders.createOptimized("Connection");
public static final CharSequence ACCEPT_ENCODING_HEADER = HttpHeaders.createOptimized("Accept-Encoding");
public static final CharSequence X_OPENRTB_VERSION_HEADER = HttpHeaders.createOptimized("x-openrtb-version");
public static final CharSequence X_PREBID_HEADER = HttpHeaders.createOptimized("x-prebid");

private HttpUtil() {
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package org.prebid.server.bidder;

import com.iab.openrtb.request.App;
import com.iab.openrtb.request.BidRequest;
import io.vertx.core.MultiMap;
import io.vertx.core.http.CaseInsensitiveHeaders;
import org.junit.Before;
import org.junit.Test;
import org.prebid.server.proto.openrtb.ext.request.ExtApp;
import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid;
import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel;

import static org.assertj.core.api.Assertions.assertThat;

public class HttpBidderRequestEnricherTest {

private HttpBidderRequestEnricher requestEnricher;

@Before
public void setUp() {

requestEnricher = new HttpBidderRequestEnricher("1.00");
}

@Test
public void shouldSendPopulatedPostRequest() {
// given
final MultiMap headers = new CaseInsensitiveHeaders();
headers.add("header1", "value1");
headers.add("header2", "value2");

// when
final MultiMap resultHeaders =
requestEnricher.enrichHeaders(headers, new CaseInsensitiveHeaders(), BidRequest.builder().build());

// then
final MultiMap expectedHeaders = new CaseInsensitiveHeaders();
expectedHeaders.addAll(headers);
expectedHeaders.add("x-prebid", "pbs-java/1.00");
assertThat(resultHeaders).hasSize(3);
assertThat(isEqualsMultiMaps(resultHeaders, expectedHeaders)).isTrue();
}

@Test
public void shouldAddSecGpcHeaderFromOriginalRequest() {
// given
final MultiMap originalHeaders = new CaseInsensitiveHeaders().add("Sec-GPC", "1");

// when
final MultiMap resultHeaders =
requestEnricher.enrichHeaders(new CaseInsensitiveHeaders(),
originalHeaders, BidRequest.builder().build());

// then
assertThat(resultHeaders.contains("Sec-GPC")).isTrue();
assertThat(resultHeaders.get("Sec-GPC")).isEqualTo("1");
}

@Test
public void shouldNotOverrideHeadersFromBidRequest() {
// given
final MultiMap originalHeaders = new CaseInsensitiveHeaders().add("Sec-GPC", "1");
final MultiMap bidderRequestHeaders = new CaseInsensitiveHeaders().add("Sec-GPC", "0");

// when
final MultiMap resultHeaders = requestEnricher.enrichHeaders(bidderRequestHeaders,
originalHeaders, BidRequest.builder().build());

// then
assertThat(resultHeaders.contains("Sec-GPC")).isTrue();
assertThat(resultHeaders.getAll("Sec-GPC")).hasSize(1);
assertThat(resultHeaders.get("Sec-GPC")).isEqualTo("0");
}

@Test
public void shouldCreateXPrebidHeaderForOutgoingRequest() {
// given
final BidRequest bidRequest = BidRequest.builder()
.ext(ExtRequest.of(ExtRequestPrebid.builder()
.channel(ExtRequestPrebidChannel.of("pbjs", "4.39"))
.build()))
.app(App.builder()
.ext(ExtApp.of(ExtAppPrebid.of("prebid-mobile", "1.2.3"), null))
.build())
.build();

// when
final MultiMap resultHeaders = requestEnricher.enrichHeaders(new CaseInsensitiveHeaders(),
new CaseInsensitiveHeaders(), bidRequest);

// then
final MultiMap expectedHeaders = new CaseInsensitiveHeaders();
expectedHeaders.add("x-prebid", "pbjs/4.39,prebid-mobile/1.2.3,pbs-java/1.00");
assertThat(isEqualsMultiMaps(resultHeaders, expectedHeaders)).isTrue();
}

private static boolean isEqualsMultiMaps(MultiMap left, MultiMap right) {
return left.size() == right.size() && left.entries().stream()
.allMatch(entry -> right.contains(entry.getKey(), entry.getValue(), true));
}
}
Loading

0 comments on commit ba338a6

Please sign in to comment.