Skip to content

Port Floxis: New Adapter#4529

Open
floxis-admin wants to merge 7 commits into
prebid:masterfrom
floxis-admin:floxis-new-adapter
Open

Port Floxis: New Adapter#4529
floxis-admin wants to merge 7 commits into
prebid:masterfrom
floxis-admin:floxis-new-adapter

Conversation

@floxis-admin

@floxis-admin floxis-admin commented Jun 3, 2026

Copy link
Copy Markdown

🔧 Type of changes

  • new bid adapter

✨ Whats the context?

This PR ports the Floxis bid adapter from PBS-Go to PBS-Java. Floxis is an ad exchange; the adapter sends standard OpenRTB 2.x server-to-server bid requests to the Floxis exchange so PBS publishers can access Floxis demand. It is the Java counterpart of the in-review Go adapter prebid/prebid-server#4811.

Scope:

  • Media types: banner, video, native, audio (declared for both app and site).
  • OpenRTB 2.6; GPP supported. The incoming bid request body is forwarded unchanged.
  • Cookie sync: redirect type.
  • modifying-vast-xml-allowed: false.

🧠 Rationale behind the change

Bidder params (imp[].ext.prebid.bidder.floxis):

  • seat (string, required, minLength 1) — the Floxis seat the publisher buys through; appended url-escaped to the endpoint as ?seat=.
  • region (string, optional, default us-e) — the regional RTB host, used as a subdomain label. Validated as a DNS label by the params JSON schema.
  • partner (string, optional, default floxis) — white-label partner, used as an additional subdomain label. Validated as a DNS label by the params JSON schema.

Endpoint routing: the fixed parent domain (.floxis.tech) is pinned in the adapter's endpoint config; only the {{Host}} subdomain is filled from the request. The subdomain is region (e.g. us-e.floxis.tech), or partner-region for a named white-label partner (e.g. acme-us-e.floxis.tech); the default floxis partner adds no prefix. region/partner are constrained to DNS labels by the params schema and url-encoded before interpolation, so the host can never be derived from a request-supplied hostname — satisfying the "no fully dynamic hostnames" requirement. seat/region/partner are read from the first imp; a multi-imp request that mixes seats/regions/partners is rejected as bad input (one request routes to one host). The request body is forwarded unchanged (no caller-owned object is mutated).

Bid type resolution treats bid.mtype (OpenRTB 2.6) as authoritative (1/2/3/4 → banner/video/audio/native). When mtype is absent, a single-format imp resolves to that media type; an imp without exactly one format cannot be disambiguated and surfaces a badServerResponse, while other bids in the response continue to be processed.

gvlVendorID is set to 1609 — Floxis (legal entity Ad Tech Company OÜ) is registered as an IAB TCF vendor with GVL ID 1609, so EU/TCF consent is enforced against that vendor ID.

🔎 New Bid Adapter Checklist

  • verify email contact works
  • NO fully dynamic hostnames
  • geographic host parameters are NOT required
  • direct use of HTTP is prohibited - implement an existing Bidder interface that will do all the job
  • if the ORTB is just forwarded to the endpoint, use the generic adapter - the adapter is not a plain forward: it performs region/partner-based host routing, url-escaped seat injection, and mtype/single-format bid-type resolution, so a dedicated adapter is warranted
  • cover an adapter configuration with an integration test

🧪 Test plan

  • FloxisBidderTest — unit tests covering region/partner host resolution (default, explicit region, partner-prefixed host), url-encoding of the seat and the host parts, request-forwarded-unchanged, the per-imp consistency check, and the full getMediaTypeForBid matrix (mtype-authoritative for all four types, unsupported mtype, single-format fallback for all four, the non-single-format error, imp-not-found), plus per-bid error accumulation. JaCoCo reports ~100% line coverage on FloxisBidder.
  • FloxisTest integration test — exercises the full auction path against WireMock and validates the adapter configuration is reachable.
  • mvn test (unit + IT) and mvn checkstyle:check pass locally.

🏎 Quality check

  • Are your changes following our code style guidelines?
  • Are there any breaking changes in your code? — No.
  • Does your test coverage exceed 90%? — Yes (~100% line on the bidder).
  • Are there any erroneous console logs, debuggers or leftover code in your changes? — No.

Companion Go adapter: prebid/prebid-server#4811. The corresponding prebid.github.io documentation PR is forthcoming.

Please add the do not port label per the porting guide (I do not have label permissions on this repo).

…sistency

seat lacked the explicit @JsonProperty that region and partner carry; add it so all three params are annotated uniformly (no behavioural change — seat already maps to the 'seat' key).
…tner are subdomain labels)

Per the dev-guide, a bidder endpoint domain must not be fully variable. Move the fixed
.floxis.tech suffix into the bidder-config endpoint and have resolveBidHost return just the
validated region/partner subdomain label. Resolved URLs are unchanged (e.g.
https://us-e.floxis.tech/pbs); matches the fixed-suffix pattern of merged region-routed
adapters (rubicon, clydo, mediago, algorix).
Comment on lines +57 to +59
if (CollectionUtils.isEmpty(request.getImp())) {
return Result.withError(BidderError.badInput("no impressions in the bid request"));
}

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.

No need for this check

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.

Good point — removed. The framework already guarantees at least one imp by the time this runs.

Comment on lines +79 to +81
// A single request routes to one Floxis host/seat (the seat is the URL query key, partner+region
// the host). All imps must therefore share the same seat, region and partner; a mismatch is a
// misconfigured request rather than something to silently route on imp[0]'s key.

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.

Remove comment

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.

Removed.

}
}
return first;
}

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.

Let's replace this method with smth like this:

    @Override
    public Result<List<HttpRequest<Void>>> makeHttpRequests(BidRequest bidRequest) {
        final List<Imp> imps = bidRequest.getImp();
        final ExtImpFlipp firstImpExt;
        try {
            firstImpExt = parseImpExt(imps.getFirst());
        } catch (PreBidException e) {
            return Result.withError(BidderError.badInput(e.getMessage()));
        }

        try {
            for (int i = 1; i < imps.size(); i++) {
                final ExtImpFlipp impExt = parseImpExt(imps.get(i));
                validateImpExt(impExt, firstImpExt);
            }
        } catch (PreBidException e) {
            return Result.withError(BidderError.badInput(e.getMessage()));
        }

       ...
   }

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 — reworked it into the parse-first-then-validate-the-rest shape you suggested.

Comment on lines +19 to +20
@PropertySource(value = "classpath:/bidder-config/floxis.yaml",
factory = YamlPropertySourceFactory.class)

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.

one-line it

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.

Comment on lines +9 to +16
@JsonProperty("seat")
String seat;

@JsonProperty("region")
String region;

@JsonProperty("partner")
String partner;

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.

No need for @JsonProperty here

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.

Removed — the field names already match the JSON keys, so they weren't needed.

Comment on lines +43 to +44
// region/partner are interpolated into the request host, so each must be a valid DNS label —
// otherwise a value carrying URL delimiters could rewrite the request origin.

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.

remove comment

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.

Removed.

Comment on lines +117 to +118
? resolvedRegion
: resolvedPartner + "-" + resolvedRegion;

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.

Encode dynamic parts

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 — the host parts go through HttpUtil.encodeUrl now as well.

Comment on lines +140 to +159
if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
return Result.empty();
}

final List<BidderError> errors = new ArrayList<>();
final List<BidderBid> bids = new ArrayList<>();
for (SeatBid seatBid : bidResponse.getSeatbid()) {
if (seatBid == null || CollectionUtils.isEmpty(seatBid.getBid())) {
continue;
}
for (Bid bid : seatBid.getBid()) {
try {
bids.add(BidderBid.of(bid, getMediaTypeForBid(bidRequest.getImp(), bid), bidResponse.getCur()));
} catch (PreBidException e) {
errors.add(BidderError.badServerResponse(e.getMessage()));
}
}
}

return Result.of(bids, errors);

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.

extract to separate method (see other bidders)

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.

Extracted into a separate extractBids method, matching the other bidders.

Comment on lines +162 to +164
// Resolves the bid's media type. When bid.mtype (OpenRTB 2.6) is set it is treated as
// authoritative. When unset, a single-format imp's media type is used; multi-format imps
// without mtype cannot be disambiguated and surface an error.

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.

remove comment

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.

Dropped it.

Comment on lines +182 to +208
int formats = 0;
BidType resolved = null;
if (imp.getBanner() != null) {
formats++;
resolved = BidType.banner;
}
if (imp.getVideo() != null) {
formats++;
resolved = BidType.video;
}
if (imp.getAudio() != null) {
formats++;
resolved = BidType.audio;
}
if (imp.getXNative() != null) {
formats++;
resolved = BidType.xNative;
}
if (formats == 1) {
return resolved;
} else if (formats > 1) {
throw new PreBidException(
"bid for multi-format imp %s requires bid.mtype to disambiguate".formatted(bid.getImpid()));
} else {
throw new PreBidException(
"unable to resolve media type for impression %s".formatted(bid.getImpid()));
}

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.

Add countFormats method.
if count != 1 -> exception
else determine format

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 — added a countFormats helper; it throws unless exactly one format is present, otherwise resolves that format.

Drop redundant in-adapter validation covered elsewhere: the empty-imp guard (framework guarantees at least one imp), the region/partner DNS-label regex (already enforced by floxis.json), and the custom isBlank helper (use StringUtils.isBlank).

Restructure makeHttpRequests to parse the first imp ext then validate the rest in a loop; extract extractBids and countFormats helpers; URL-encode the dynamic host parts; drop redundant @JsonProperty on ExtImpFloxis and one-line @propertysource; remove explanatory comments. Tests updated to match.
@floxis-admin

Copy link
Copy Markdown
Author

Thanks for the thorough review @CTMBNara — all of it is addressed in 21d1900: dropped the redundant empty-imp check, the in-code DNS regex (floxis.json already covers it) and the custom isBlank helper; reworked makeHttpRequests into the parse-first/validate-the-rest shape; extracted extractBids and countFormats; url-encoded the host parts; and cleared out the comments. Also synced the PR description with the final code. Ready for another look whenever you have a moment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants