{"record":{"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/d041d4d0fd48c3f0832084d33be16cf1c4835f85/aiohttp/web_app.py#L282-L318","documentation":"Application.add_domain(domain, subapp) requires its first argument to be a Python str. It performs an isinstance check up front because the subsequent MaskDomain/Domain rule construction and wildcard ('*') detection depend on str semantics. Any non-str value (bytes, int, yarl.URL, None) raises TypeError before any routing work is done.","triggerScenarios":"Calling app.add_domain(123, subapp), app.add_domain(b'example.com', subapp), app.add_domain(url.host, subapp) where url.host is bytes, or passing None read from config.","commonSituations":"Reading a host from environment/config as bytes; passing a yarl.URL object or its .raw_host (which is bytes); dynamically building domains from parsed URLs without str() coercion.","solutions":["Pass a plain str for domain, e.g. app.add_domain('api.example.com', subapp).","If the value originates from config/env/URL parsing, coerce it first: app.add_domain(str(domain), subapp).","For wildcard subdomains pass a string containing '*', e.g. '*.example.com'."],"exampleFix":"// before\napp.add_domain(cfg['HOST'], subapp)   # cfg['HOST'] is bytes\n// after\napp.add_domain(str(cfg['HOST']), subapp)","handlingStrategy":"type-guard","validationCode":"if not isinstance(domain, str):\n    domain = str(domain)\napp.add_domain(domain, subapp)","typeGuard":"def is_valid_domain(d: object) -> TypeGuard[str]:\n    return isinstance(d, str)","tryCatchPattern":null,"preventionTips":["Always pass a literal str to add_domain.","Coerce values from config/env/URL parsing with str() before the call.","Use mypy/pyright to type the domain parameter as str at the call site."],"tags":["type-error","routing","subapp","domain"],"backgroundTag":null,"analyzedSha":"d041d4d0fd48c3f0832084d33be16cf1c4835f85","analyzedAt":"2026-08-11T20:44:15.550Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}