aio-libs/aiohttp · error · TypeError

Domain must be str

Error message

Domain must be str

What it means

Raised as TypeError by Domain.validation when the domain argument is not a str. Domain (used in Domain and MaskDomain for sub-app matching by host) requires a textual hostname, so non-string inputs (bytes, int, None) are rejected before any parsing.

Source

Thrown at aiohttp/web_urldispatcher.py:781

    @abc.abstractmethod  # pragma: no branch
    def canonical(self) -> str:
        """Return a str"""


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)

View on GitHub (pinned to c0ef574e29)

Solutions

  1. Pass the host as a string: Domain('example.com').
  2. Coerce config values: Domain(str(host)) after loading.
  3. Validate host is a non-empty str before constructing Domain.

Example fix

# before
app.add_domain(8080, sub_app)  # port int instead of host str

# after
app.add_domain('example.com', sub_app)
Defensive patterns

Strategy: type-guard

Validate before calling

def coerce_domain(domain):
    if not isinstance(domain, str):
        raise TypeError(f"Domain must be str, got {type(domain).__name__}")
    return domain

Type guard

def is_domain_str(domain) -> bool:
    return isinstance(domain, str)

Try / catch

try:
    app.add_domain(host, sub_app)
except TypeError as e:
    if 'Domain must be str' in str(e):
        host = str(host)
        app.add_domain(host, sub_app)
    else:
        raise

Prevention

When it happens

Trigger: Constructing aiohttp.web.Domain(123), Domain(b'example.com'), Domain(None), or passing a host read from config that was parsed as a non-string type. Used via app.add_domain(host, sub_app).

Common situations: Config loaded from YAML/JSON where the host was unquoted and parsed as a number; passing bytes from a network parser; None when a host variable was unset.

Related errors


AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04). Data as JSON: /data/errors/47cb43fc3ac6fe16.json. Report an issue: GitHub.