Repro
Send a v2.5 get_products request with a brand_manifest URL that includes a path:
client.call_tool("get_products", {"brand_manifest": "https://acme.com/.well-known/brand.json", "brief": "..."})
Observed
INVALID_REQUEST[brand.domain]: get_products failed:
Invalid value for field 'brand.domain':
String should match pattern '^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$'
Root cause
In adcp/compat/legacy/v2_5/get_products.py:130-133:
# brand_manifest (v2.5 URL string) → brand.domain (v3 BrandReference)
brand_manifest = out.pop("brand_manifest", None)
if isinstance(brand_manifest, str) and brand_manifest and "brand" not in out:
out["brand"] = {"domain": strip_url_scheme(brand_manifest)}
strip_url_scheme (in adcp/compat/legacy/v2_5/_url.py) only strips the scheme prefix and trailing slash. For https://acme.com/.well-known/brand.json it returns acme.com/.well-known/brand.json — which has slashes and dots in positions that fail BrandReference.domain's regex.
The SDK's own _normalize_brand_manifest in adcp/server/translate.py:485 (the utility intended for adopters to call manually) gets this right — it uses urlparse(manifest).hostname. Same concept, different impl, only one of the two extracts the hostname correctly.
Spec context
brand_manifest per the v2.5 spec is documented as "https://example.com/brand.json" — i.e. a URL to a JSON file, which by construction has a path. So the legitimate input shape is exactly the one this adapter breaks.
Impact
Surfaced via shape-based legacy detection (#673) in 5.2.0 — any v2.5 buyer (untagged or otherwise) sending a brand_manifest URL with a path gets a confusing brand.domain regex error. Trapped us on a CI run where the test had been passing under 5.1.0 (where brand_manifest got silently stripped as an unknown field, no translation attempted).
Suggested fix
from urllib.parse import urlparse
if isinstance(brand_manifest, str) and brand_manifest and "brand" not in out:
parsed = urlparse(brand_manifest)
domain = parsed.hostname or strip_url_scheme(brand_manifest)
out["brand"] = {"domain": domain}
Same shape as _normalize_brand_manifest in translate.py. The strip_url_scheme fallback covers the case where the buyer sent a bare domain without :// (no scheme → hostname is None).
Also applies to adapt_request in create_media_buy.py and update_media_buy.py if they take the same path (haven't checked).
Environment
adcp==5.2.0
- v2.5 wire-version buyer (or shape-detected via
is_legacy_shape)
Repro
Send a v2.5
get_productsrequest with abrand_manifestURL that includes a path:Observed
Root cause
In
adcp/compat/legacy/v2_5/get_products.py:130-133:strip_url_scheme(inadcp/compat/legacy/v2_5/_url.py) only strips the scheme prefix and trailing slash. Forhttps://acme.com/.well-known/brand.jsonit returnsacme.com/.well-known/brand.json— which has slashes and dots in positions that failBrandReference.domain's regex.The SDK's own
_normalize_brand_manifestinadcp/server/translate.py:485(the utility intended for adopters to call manually) gets this right — it usesurlparse(manifest).hostname. Same concept, different impl, only one of the two extracts the hostname correctly.Spec context
brand_manifestper the v2.5 spec is documented as"https://example.com/brand.json"— i.e. a URL to a JSON file, which by construction has a path. So the legitimate input shape is exactly the one this adapter breaks.Impact
Surfaced via shape-based legacy detection (#673) in 5.2.0 — any v2.5 buyer (untagged or otherwise) sending a
brand_manifestURL with a path gets a confusingbrand.domainregex error. Trapped us on a CI run where the test had been passing under 5.1.0 (wherebrand_manifestgot silently stripped as an unknown field, no translation attempted).Suggested fix
Same shape as
_normalize_brand_manifestintranslate.py. Thestrip_url_schemefallback covers the case where the buyer sent a bare domain without://(no scheme →hostnameisNone).Also applies to
adapt_requestincreate_media_buy.pyandupdate_media_buy.pyif they take the same path (haven't checked).Environment
adcp==5.2.0is_legacy_shape)