Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement x-prebid headers support #1301

Merged
merged 7 commits into from
Jun 11, 2021
Merged
Show file tree
Hide file tree
Changes from 5 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
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) {
this.pbsRecord = createNameVersionRecord("pbs-java", Objects.requireNonNull(pbsVersion));
rpanchyk marked this conversation as resolved.
Show resolved Hide resolved
}

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);

originalRequestHeaders.entries().stream()
.filter(entry -> HEADERS_TO_COPY.contains(entry.getKey())
&& !bidderRequestHeadersCopy.contains(entry.getKey()))
.forEach(entry -> bidderRequestHeadersCopy.add(entry.getKey(), entry.getValue()));
rpanchyk marked this conversation as resolved.
Show resolved Hide resolved
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 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