{"id":"47cb43fc3ac6fe16","repo":"aio-libs/aiohttp","slug":"domain-must-be-str-47cb43","errorCode":null,"errorMessage":"Domain must be str","messagePattern":"Domain must be str","errorType":"validation","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"aiohttp/web_urldispatcher.py","lineNumber":781,"sourceCode":"    @abc.abstractmethod  # pragma: no branch\n    def canonical(self) -> str:\n        \"\"\"Return a str\"\"\"\n\n\nclass Domain(AbstractRuleMatching):\n    re_part = re.compile(r\"(?!-)[a-z\\d-]{1,63}(?<!-)\")\n\n    def __init__(self, domain: str) -> None:\n        super().__init__()\n        self._domain = self.validation(domain)\n\n    @property\n    def canonical(self) -> str:\n        return self._domain\n\n    def validation(self, domain: str) -> str:\n        if not isinstance(domain, str):\n            raise TypeError(\"Domain must be str\")\n        domain = domain.rstrip(\".\").lower()\n        if not domain:\n            raise ValueError(\"Domain cannot be empty\")\n        elif \"://\" in domain:\n            raise ValueError(\"Scheme not supported\")\n        url = URL(\"http://\" + domain)\n        assert url.raw_host is not None\n        if not all(self.re_part.fullmatch(x) for x in url.raw_host.split(\".\")):\n            raise ValueError(\"Domain not valid\")\n        if url.port == 80:\n            return url.raw_host\n        return f\"{url.raw_host}:{url.port}\"\n\n    async def match(self, request: Request) -> bool:\n        host = request.headers.get(hdrs.HOST)\n        if not host:\n            return False\n        return self.match_domain(host)","sourceCodeStart":763,"sourceCodeEnd":799,"githubUrl":"https://github.com/aio-libs/aiohttp/blob/c0ef574e29109210e96e652771ae4e7b88615fa4/aiohttp/web_urldispatcher.py#L763-L799","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Pass the host as a string: Domain('example.com').","Coerce config values: Domain(str(host)) after loading.","Validate host is a non-empty str before constructing Domain."],"exampleFix":"# before\napp.add_domain(8080, sub_app)  # port int instead of host str\n\n# after\napp.add_domain('example.com', sub_app)","handlingStrategy":"type-guard","validationCode":"def coerce_domain(domain):\n    if not isinstance(domain, str):\n        raise TypeError(f\"Domain must be str, got {type(domain).__name__}\")\n    return domain","typeGuard":"def is_domain_str(domain) -> bool:\n    return isinstance(domain, str)","tryCatchPattern":"try:\n    app.add_domain(host, sub_app)\nexcept TypeError as e:\n    if 'Domain must be str' in str(e):\n        host = str(host)\n        app.add_domain(host, sub_app)\n    else:\n        raise","preventionTips":["Coerce config values to str before passing to add_domain/Domain.","Validate host config keys are strings in config loading.","Annotate host variables as str."],"tags":["aiohttp","domain-matching","type-error","sub-app","configuration"],"analyzedSha":"c0ef574e29109210e96e652771ae4e7b88615fa4","analyzedAt":"2026-08-04T19:51:05.467Z","schemaVersion":2}