aio-libs/aiohttp · error · ValueError

Domain cannot be empty

Error message

Domain cannot be empty

What it means

After confirming the domain is a str, DomainValidator strips trailing dots and lowercases; if the result is empty (e.g. the input was '' or only dots like '...'), it raises ValueError. An empty domain cannot match any Host header, so the rule would be a no-op — aiohttp rejects it at construction to surface the misconfiguration immediately.

Solutions

  1. Provide a non-empty domain string after stripping dots.
  2. Validate config at startup — fail loudly if the domain env var is empty rather than passing it through to Domain().
  3. Default to a concrete hostname (e.g. 'localhost' for dev) when the config value is missing.

Example fix

// before
import os
rule = Domain(os.environ.get('DOMAIN', ''))  # '' if unset → ValueError
// after
import os
domain = os.environ.get('DOMAIN')
if not domain:
    raise SystemExit('DOMAIN env var must be set')
rule = Domain(domain)
Defensive patterns

Strategy: validation

Validate before calling

def non_empty_domain(domain: str) -> str:
    cleaned = domain.rstrip('.').lower()
    if not cleaned:
        raise ValueError('Domain is empty after stripping dots')
    return cleaned

Prevention

When it happens

Trigger: Constructing a Domain rule with Domain(''), Domain('.'), Domain('...'), or a string that becomes empty after stripping trailing dots. Typically arises from misconfigured host-based routing where the domain comes from an empty config value.

Common situations: Environment variable for the domain is unset (defaults to ''); config file with a blank domain field; dynamic construction that yields an empty string when a lookup fails; trailing-dot-only strings from malformed input.

Related errors


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

Appendix: source

Thrown at aiohttp/web_urldispatcher.py:784


class Domain(AbstractRuleMatching):
    re_part = re.compile(r"(?!-)[a-z\d-]{1,63}(?<!-)")

    def __init__(self, domain: str) -> None:
        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

View on GitHub (pinned to d041d4d0fd)