Skip to content

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

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)

Activity

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

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