Skip to content

Commit 8763dcd

Browse files
Floxis: mirror Prebid.js seat/region/partner host logic
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 454913b commit 8763dcd

4 files changed

Lines changed: 123 additions & 33 deletions

File tree

src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525

2626
import java.util.ArrayList;
2727
import java.util.List;
28-
import java.util.Map;
2928
import java.util.Objects;
29+
import java.util.regex.Pattern;
3030

3131
public class FloxisBidder implements Bidder<BidRequest> {
3232

@@ -37,15 +37,12 @@ public class FloxisBidder implements Bidder<BidRequest> {
3737
private static final String HOST_MACRO = "{{Host}}";
3838
private static final String SEAT_MACRO = "{{SeatId}}";
3939

40-
// Fixed allowlist mapping the bidder's region param to a Floxis RTB host. Routing is
41-
// never derived from request-supplied hostnames; an unknown or empty region falls back
42-
// to us-e.
43-
private static final Map<String, String> REGION_HOSTS = Map.of(
44-
"us-e", "rtb-us-e.floxis.tech",
45-
"eu", "rtb-eu.floxis.tech",
46-
"apac", "rtb-apac.floxis.tech");
47-
4840
private static final String DEFAULT_REGION = "us-e";
41+
private static final String DEFAULT_PARTNER = "floxis";
42+
43+
// region/partner are interpolated into the request host, so each must be a valid DNS label —
44+
// otherwise a value carrying URL delimiters could rewrite the request origin.
45+
private static final Pattern HOST_LABEL = Pattern.compile("^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$");
4946

5047
private final String endpointUrl;
5148
private final JacksonMapper mapper;
@@ -61,35 +58,37 @@ public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request
6158
return Result.withError(BidderError.badInput("no impressions in the bid request"));
6259
}
6360

64-
final ExtImpFloxis extImp;
61+
final String uri;
6562
try {
66-
extImp = resolveCommonImpExt(request.getImp());
63+
uri = resolveUrl(endpointUrl, resolveCommonImpExt(request.getImp()));
6764
} catch (PreBidException e) {
6865
return Result.withError(BidderError.badInput(e.getMessage()));
6966
}
7067

7168
// The request body is forwarded unchanged; no caller-owned struct is mutated.
7269
return Result.withValue(HttpRequest.<BidRequest>builder()
7370
.method(HttpMethod.POST)
74-
.uri(resolveUrl(endpointUrl, extImp))
71+
.uri(uri)
7572
.headers(HttpUtil.headers())
7673
.impIds(BidderUtil.impIds(request))
7774
.payload(request)
7875
.body(mapper.encodeToBytes(request))
7976
.build());
8077
}
8178

82-
// A single request routes to one Floxis host/seat (the seat is the URL query key). All imps
83-
// must therefore share the same seat and region; a mismatch is a misconfigured request rather
84-
// than something to silently route on imp[0]'s key.
79+
// A single request routes to one Floxis host/seat (the seat is the URL query key, partner+region
80+
// the host). All imps must therefore share the same seat, region and partner; a mismatch is a
81+
// misconfigured request rather than something to silently route on imp[0]'s key.
8582
private ExtImpFloxis resolveCommonImpExt(List<Imp> imps) {
8683
final ExtImpFloxis first = parseImpExt(imps.getFirst());
8784
for (Imp imp : imps.subList(1, imps.size())) {
8885
final ExtImpFloxis current = parseImpExt(imp);
8986
if (!Objects.equals(current.getSeat(), first.getSeat())
90-
|| !Objects.equals(current.getRegion(), first.getRegion())) {
87+
|| !Objects.equals(current.getRegion(), first.getRegion())
88+
|| !Objects.equals(current.getPartner(), first.getPartner())) {
9189
throw new PreBidException(
92-
"all impressions must target the same Floxis seat and region; imp %s differs from imp %s"
90+
"all impressions must target the same Floxis seat, region and partner; "
91+
+ "imp %s differs from imp %s"
9392
.formatted(imp.getId(), imps.getFirst().getId()));
9493
}
9594
}
@@ -104,14 +103,27 @@ private ExtImpFloxis parseImpExt(Imp imp) {
104103
}
105104
}
106105

107-
private static String resolveHost(String region) {
108-
final String host = region == null ? null : REGION_HOSTS.get(region);
109-
return host != null ? host : REGION_HOSTS.get(DEFAULT_REGION);
106+
// Bidding host: the supply partner's regional subdomain (floxis itself has no partner prefix).
107+
private static String resolveBidHost(String region, String partner) {
108+
final String resolvedRegion = isBlank(region) ? DEFAULT_REGION : region;
109+
final String resolvedPartner = isBlank(partner) ? DEFAULT_PARTNER : partner;
110+
if (!HOST_LABEL.matcher(resolvedRegion).matches() || !HOST_LABEL.matcher(resolvedPartner).matches()) {
111+
throw new PreBidException(
112+
"invalid Floxis region or partner; both must be DNS labels: region=%s partner=%s"
113+
.formatted(resolvedRegion, resolvedPartner));
114+
}
115+
return resolvedPartner.equals(DEFAULT_PARTNER)
116+
? resolvedRegion + ".floxis.tech"
117+
: resolvedPartner + "-" + resolvedRegion + ".floxis.tech";
118+
}
119+
120+
private static boolean isBlank(String value) {
121+
return value == null || value.isEmpty();
110122
}
111123

112124
private static String resolveUrl(String endpoint, ExtImpFloxis extImp) {
113125
return endpoint
114-
.replace(HOST_MACRO, resolveHost(extImp.getRegion()))
126+
.replace(HOST_MACRO, resolveBidHost(extImp.getRegion(), extImp.getPartner()))
115127
.replace(SEAT_MACRO, HttpUtil.encodeUrl(extImp.getSeat()));
116128
}
117129

src/main/java/org/prebid/server/proto/openrtb/ext/request/floxis/ExtImpFloxis.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,7 @@ public class ExtImpFloxis {
1010

1111
@JsonProperty("region")
1212
String region;
13+
14+
@JsonProperty("partner")
15+
String partner;
1316
}

src/main/resources/static/bidder-params/floxis.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,13 @@
1212
},
1313
"region": {
1414
"type": "string",
15-
"enum": ["us-e", "eu", "apac"],
16-
"description": "The Floxis RTB region; defaults to us-e when omitted"
15+
"pattern": "^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$",
16+
"description": "The Floxis region (DNS label, interpolated into the bidding host); defaults to us-e when omitted"
17+
},
18+
"partner": {
19+
"type": "string",
20+
"pattern": "^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$",
21+
"description": "The white-label partner (DNS label, interpolated into the bidding host); defaults to floxis when omitted"
1722
}
1823
},
1924
"required": ["seat"]

src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public void makeHttpRequestsShouldUrlEscapeSeatAndUseEuHost() {
9090
assertThat(result.getErrors()).isEmpty();
9191
assertThat(result.getValue()).hasSize(1)
9292
.extracting(HttpRequest::getUri)
93-
.containsExactly("https://rtb-eu.floxis.tech/pbs?seat=a+b%26c");
93+
.containsExactly("https://eu.floxis.tech/pbs?seat=a+b%26c");
9494
}
9595

9696
@Test
@@ -105,7 +105,7 @@ public void makeHttpRequestsShouldUseApacHost() {
105105
assertThat(result.getErrors()).isEmpty();
106106
assertThat(result.getValue()).hasSize(1)
107107
.extracting(HttpRequest::getUri)
108-
.containsExactly("https://rtb-apac.floxis.tech/pbs?seat=seat-apac");
108+
.containsExactly("https://apac.floxis.tech/pbs?seat=seat-apac");
109109
}
110110

111111
@Test
@@ -120,7 +120,7 @@ public void makeHttpRequestsShouldUseUseHostForExplicitUsE() {
120120
assertThat(result.getErrors()).isEmpty();
121121
assertThat(result.getValue()).hasSize(1)
122122
.extracting(HttpRequest::getUri)
123-
.containsExactly("https://rtb-us-e.floxis.tech/pbs?seat=abc");
123+
.containsExactly("https://us-e.floxis.tech/pbs?seat=abc");
124124
}
125125

126126
@Test
@@ -135,11 +135,11 @@ public void makeHttpRequestsShouldDefaultToUseHostWhenRegionMissing() {
135135
assertThat(result.getErrors()).isEmpty();
136136
assertThat(result.getValue()).hasSize(1)
137137
.extracting(HttpRequest::getUri)
138-
.containsExactly("https://rtb-us-e.floxis.tech/pbs?seat=abc");
138+
.containsExactly("https://us-e.floxis.tech/pbs?seat=abc");
139139
}
140140

141141
@Test
142-
public void makeHttpRequestsShouldDefaultToUseHostWhenRegionUnknown() {
142+
public void makeHttpRequestsShouldUseArbitraryValidRegionLabelAsSubdomain() {
143143
// given
144144
final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("abc", "mars")));
145145

@@ -150,7 +150,50 @@ public void makeHttpRequestsShouldDefaultToUseHostWhenRegionUnknown() {
150150
assertThat(result.getErrors()).isEmpty();
151151
assertThat(result.getValue()).hasSize(1)
152152
.extracting(HttpRequest::getUri)
153-
.containsExactly("https://rtb-us-e.floxis.tech/pbs?seat=abc");
153+
.containsExactly("https://mars.floxis.tech/pbs?seat=abc");
154+
}
155+
156+
@Test
157+
public void makeHttpRequestsShouldReturnBadInputWhenRegionIsNotAValidHostLabel() {
158+
// given
159+
final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("abc", "evil.com/x?")));
160+
161+
// when
162+
final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
163+
164+
// then
165+
assertThat(result.getValue()).isEmpty();
166+
assertThat(result.getErrors()).hasSize(1)
167+
.allSatisfy(error -> assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input));
168+
}
169+
170+
@Test
171+
public void makeHttpRequestsShouldUsePartnerPrefixedHostWhenPartnerProvided() {
172+
// given
173+
final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("abc", "us-e", "acme")));
174+
175+
// when
176+
final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
177+
178+
// then
179+
assertThat(result.getErrors()).isEmpty();
180+
assertThat(result.getValue()).hasSize(1)
181+
.extracting(HttpRequest::getUri)
182+
.containsExactly("https://acme-us-e.floxis.tech/pbs?seat=abc");
183+
}
184+
185+
@Test
186+
public void makeHttpRequestsShouldReturnBadInputWhenPartnerIsNotAValidHostLabel() {
187+
// given
188+
final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("abc", "us-e", "evil.com/x?")));
189+
190+
// when
191+
final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
192+
193+
// then
194+
assertThat(result.getValue()).isEmpty();
195+
assertThat(result.getErrors()).hasSize(1)
196+
.allSatisfy(error -> assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input));
154197
}
155198

156199
@Test
@@ -171,7 +214,7 @@ public void makeHttpRequestsShouldRouteOnceAndForwardAllImpsWhenSeatAndRegionMat
171214
assertThat(result.getErrors()).isEmpty();
172215
assertThat(result.getValue()).hasSize(1)
173216
.extracting(HttpRequest::getUri)
174-
.containsExactly("https://rtb-eu.floxis.tech/pbs?seat=seat-eu");
217+
.containsExactly("https://eu.floxis.tech/pbs?seat=seat-eu");
175218
assertThat(result.getValue())
176219
.extracting(HttpRequest::getPayload)
177220
.flatExtracting(BidRequest::getImp)
@@ -197,7 +240,7 @@ public void makeHttpRequestsShouldReturnErrorWhenImpsTargetDifferentSeat() {
197240
assertThat(result.getValue()).isEmpty();
198241
assertThat(result.getErrors()).hasSize(1)
199242
.containsOnly(BidderError.badInput(
200-
"all impressions must target the same Floxis seat and region; "
243+
"all impressions must target the same Floxis seat, region and partner; "
201244
+ "imp imp-2 differs from imp imp-1"));
202245
}
203246

@@ -219,7 +262,29 @@ public void makeHttpRequestsShouldReturnErrorWhenImpsTargetDifferentRegion() {
219262
assertThat(result.getValue()).isEmpty();
220263
assertThat(result.getErrors()).hasSize(1)
221264
.containsOnly(BidderError.badInput(
222-
"all impressions must target the same Floxis seat and region; "
265+
"all impressions must target the same Floxis seat, region and partner; "
266+
+ "imp imp-2 differs from imp imp-1"));
267+
}
268+
269+
@Test
270+
public void makeHttpRequestsShouldReturnErrorWhenImpsTargetDifferentPartner() {
271+
// given
272+
final BidRequest bidRequest = BidRequest.builder()
273+
.id("req-1")
274+
.imp(asList(
275+
givenImp(imp -> imp.id("imp-1").ext(givenImpExt("seat-eu", "eu", "acme"))),
276+
givenImp(imp -> imp.id("imp-2").ext(givenImpExt("seat-eu", "eu", "other")))))
277+
.site(Site.builder().id("271").build())
278+
.build();
279+
280+
// when
281+
final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
282+
283+
// then
284+
assertThat(result.getValue()).isEmpty();
285+
assertThat(result.getErrors()).hasSize(1)
286+
.containsOnly(BidderError.badInput(
287+
"all impressions must target the same Floxis seat, region and partner; "
223288
+ "imp imp-2 differs from imp imp-1"));
224289
}
225290

@@ -448,7 +513,12 @@ private static Imp givenImp(UnaryOperator<Imp.ImpBuilder> impCustomizer) {
448513
}
449514

450515
private static com.fasterxml.jackson.databind.node.ObjectNode givenImpExt(String seat, String region) {
451-
return mapper.valueToTree(ExtPrebid.of(null, ExtImpFloxis.of(seat, region)));
516+
return givenImpExt(seat, region, null);
517+
}
518+
519+
private static com.fasterxml.jackson.databind.node.ObjectNode givenImpExt(String seat, String region,
520+
String partner) {
521+
return mapper.valueToTree(ExtPrebid.of(null, ExtImpFloxis.of(seat, region, partner)));
452522
}
453523

454524
private static BidResponse givenBidResponse(Bid bid) {

0 commit comments

Comments
 (0)