{"id":"bd1d5e4d0e8de741","repo":"aio-libs/aiohttp","slug":"domain-must-be-str","errorCode":null,"errorMessage":"Domain must be str","messagePattern":"Domain must be str","errorType":"validation","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"aiohttp/web_app.py","lineNumber":300,"sourceCode":"        return self._add_subapp(factory, subapp)\n\n    def _add_subapp(\n        self, resource_factory: Callable[[], _Resource], subapp: \"Application\"\n    ) -> _Resource:\n        if self.frozen:\n            raise RuntimeError(\"Cannot add sub application to frozen application\")\n        if subapp.frozen:\n            raise RuntimeError(\"Cannot add frozen application\")\n        resource = resource_factory()\n        self.router.register_resource(resource)\n        self._reg_subapp_signals(subapp)\n        self._subapps.append(subapp)\n        subapp.pre_freeze()\n        return resource\n\n    def add_domain(self, domain: str, subapp: \"Application\") -> MatchedSubAppResource:\n        if not isinstance(domain, str):\n            raise TypeError(\"Domain must be str\")\n        elif \"*\" in domain:\n            rule: Domain = MaskDomain(domain)\n        else:\n            rule = Domain(domain)\n        factory = partial(MatchedSubAppResource, rule, subapp)\n        return self._add_subapp(factory, subapp)\n\n    def add_routes(self, routes: Iterable[AbstractRouteDef]) -> list[AbstractRoute]:\n        return self.router.add_routes(routes)\n\n    @property\n    def on_response_prepare(self) -> _RespPrepareSignal:\n        return self._on_response_prepare\n\n    @property\n    def on_startup(self) -> _AppSignal:\n        return self._on_startup\n","sourceCodeStart":282,"sourceCodeEnd":318,"githubUrl":"https://github.com/aio-libs/aiohttp/blob/c0ef574e29109210e96e652771ae4e7b88615fa4/aiohttp/web_app.py#L282-L318","documentation":"Raised by Application.add_domain when domain is not a str instance. add_domain mounts a subapp against a Host-header rule, and the rule is matched/compiled as a string (MaskDomain uses shell-style globbing on the host). A non-str domain (bytes, None, ipaddress object) cannot be matched against the Host header, so aiohttp rejects it up front with TypeError.","triggerScenarios":"Calling app.add_domain(b'api.example.com', subapp); passing an ipaddress.IPv4Address; passing None when no domain config is present; passing a parsed URL object instead of its .host attribute.","commonSituations":"Reading the domain from config that yields bytes; building from urllib.parse.urlparse and forgetting .host; templating that emits None on missing key; mixing add_domain (Host-based) with add_subapp (path-based) and confusing the argument type.","solutions":["Pass a str domain: app.add_domain('api.example.com', subapp).","If you have a URL object, use parsed.url or parsed.host.","Coerce bytes via .decode('ascii'); reject None explicitly.","Validate at config load time."],"exampleFix":"// before\napp.add_domain(b'api.example.com', subapp)  # bytes\napp.add_domain(parsed, subapp)            # urlparse result\n// after\napp.add_domain('api.example.com', subapp)\napp.add_domain(parsed.host, subapp)","handlingStrategy":"type-guard","validationCode":"def safe_add_domain(app, domain, subapp):\n    if not isinstance(domain, str):\n        raise TypeError(f'domain must be str, got {type(domain).__name__}')\n    return app.add_domain(domain, subapp)","typeGuard":"from typing import Any, TypeGuard\n\ndef is_str_domain(d: Any) -> TypeGuard[str]:\n    return isinstance(d, str)","tryCatchPattern":null,"preventionTips":["Pass host as a str literal or parsed.host.","Decode bytes domains at the config boundary.","Reject None domains explicitly at config load."],"tags":["web-app","subapp","domain","typing","validation"],"analyzedSha":"c0ef574e29109210e96e652771ae4e7b88615fa4","analyzedAt":"2026-08-04T19:51:05.467Z","schemaVersion":2}