Skip to content

v2.5 brand_manifest adapter preserves URL path, breaks brand.domain regex #677

@bokelley

Description

@bokelley

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)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions