aio-libs/aiohttp · error · ValueError

Domain not valid

Error message

Domain not valid

What it means

Thrown by Domain.validation() when, after stripping the scheme and lowercasing, the hostname's dot-separated labels each fail to match re_part (roughly a valid DNS label: alphanumerics and hyphens, max 63 chars, no leading/trailing hyphen). This guards add_domain() against malformed host patterns that could never match a real Host header. Each label must independently be a legal DNS label.

Solutions

  1. Use plain hyphen-and-alphanumeric labels only: 'api.example.com'.
  2. For wildcards, switch to a Domain subclass that uses MaskDomain (app.add_domain('*.example.com') routes through MaskDomain automatically only if configured).
  3. Validate each label with a regex like r'(?!-)[a-z0-9-]{1,63}(?<!-)' before calling add_domain.

Example fix

// before
app.add_domain('api_stage.example.com', sub_app)  # underscore rejected
// after
app.add_domain('api-stage.example.com', sub_app)
Defensive patterns

Strategy: validation

Validate before calling

import re
LABEL = re.compile(r'(?!-)[a-z0-9-]{1,63}(?<!-)')

def valid_host(host: str) -> bool:
    host = host.rstrip('.').lower()
    if '://' in host or not host:
        return False
    return all(LABEL.fullmatch(p) for p in host.split('.'))

if not valid_host(cfg_host):
    raise ValueError(f'invalid domain: {cfg_host!r}')

Type guard

def is_dns_label_string(value: str) -> bool:
    return (
        isinstance(value, str)
        and '://' not in value
        and bool(value)
        and all(
            part and len(part) <= 63 and not part.startswith('-') and not part.endswith('-')
            and part.replace('-', '').isalnum()
            for part in value.rstrip('.').lower().split('.')
        )
    )

Try / catch

try:
    app.add_domain(host, sub_app)
except ValueError as e:
    if 'Domain not valid' in str(e):
        # log and fall back to a default host or skip sub-app mount
        log.warning('skipping sub-app for invalid host %r', host)
    else:
        raise

Prevention

When it happens

Trigger: Calling add_domain('exa mple.com') (whitespace inside), add_domain('ex_ample.com') (underscore), add_domain('a' * 64 + '.com') (label too long), or add_domain('..com') (empty label). MaskDomain allows '*' but otherwise the same rule applies.

Common situations: Using underscores in hostnames (common in some internal DNS but illegal per the regex); trailing malformed characters from config parsing; wildcard patterns with '*' in the wrong position (only MaskDomain supports '*').

Related errors


AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11). Data as JSON: /api/errors/d69f2c035646e067. Report an issue: GitHub.

Appendix: source

Thrown at aiohttp/web_urldispatcher.py:790

        super().__init__()
        self._domain = self.validation(domain)

    @property
    def canonical(self) -> str:
        return self._domain

    def validation(self, domain: str) -> str:
        if not isinstance(domain, str):
            raise TypeError("Domain must be str")
        domain = domain.rstrip(".").lower()
        if not domain:
            raise ValueError("Domain cannot be empty")
        elif "://" in domain:
            raise ValueError("Scheme not supported")
        url = URL("http://" + domain)
        assert url.raw_host is not None
        if not all(self.re_part.fullmatch(x) for x in url.raw_host.split(".")):
            raise ValueError("Domain not valid")
        if url.port == 80:
            return url.raw_host
        return f"{url.raw_host}:{url.port}"

    async def match(self, request: Request) -> bool:
        host = request.headers.get(hdrs.HOST)
        if not host:
            return False
        return self.match_domain(host)

    def match_domain(self, host: str) -> bool:
        return host.lower() == self._domain

    def get_info(self) -> _InfoDict:
        return {"domain": self._domain}


class MaskDomain(Domain):

View on GitHub (pinned to d041d4d0fd)