aio-libs/aiohttp · error · ValueError
Domain not valid
Error message
Domain not valid
What it means
Thrown by Domain.validation() when, after stripping the scheme and lowercasing, the hostname's dot-separated labels each fail to match re_part (roughly a valid DNS label: alphanumerics and hyphens, max 63 chars, no leading/trailing hyphen). This guards add_domain() against malformed host patterns that could never match a real Host header. Each label must independently be a legal DNS label.
Solutions
- Use plain hyphen-and-alphanumeric labels only: 'api.example.com'.
- For wildcards, switch to a Domain subclass that uses MaskDomain (app.add_domain('*.example.com') routes through MaskDomain automatically only if configured).
- Validate each label with a regex like r'(?!-)[a-z0-9-]{1,63}(?<!-)' before calling add_domain.
Example fix
// before
app.add_domain('api_stage.example.com', sub_app) # underscore rejected
// after
app.add_domain('api-stage.example.com', sub_app) Defensive patterns
Strategy: validation
Validate before calling
import re
LABEL = re.compile(r'(?!-)[a-z0-9-]{1,63}(?<!-)')
def valid_host(host: str) -> bool:
host = host.rstrip('.').lower()
if '://' in host or not host:
return False
return all(LABEL.fullmatch(p) for p in host.split('.'))
if not valid_host(cfg_host):
raise ValueError(f'invalid domain: {cfg_host!r}') Type guard
def is_dns_label_string(value: str) -> bool:
return (
isinstance(value, str)
and '://' not in value
and bool(value)
and all(
part and len(part) <= 63 and not part.startswith('-') and not part.endswith('-')
and part.replace('-', '').isalnum()
for part in value.rstrip('.').lower().split('.')
)
) Try / catch
try:
app.add_domain(host, sub_app)
except ValueError as e:
if 'Domain not valid' in str(e):
# log and fall back to a default host or skip sub-app mount
log.warning('skipping sub-app for invalid host %r', host)
else:
raise Prevention
- Avoid underscores in hostnames; use hyphens.
- Validate each DNS label against RFC 1035 rules before add_domain.
- For wildcards, ensure you are using a rule class that supports them (MaskDomain).
When it happens
Trigger: Calling add_domain('exa mple.com') (whitespace inside), add_domain('ex_ample.com') (underscore), add_domain('a' * 64 + '.com') (label too long), or add_domain('..com') (empty label). MaskDomain allows '*' but otherwise the same rule applies.
Common situations: Using underscores in hostnames (common in some internal DNS but illegal per the regex); trailing malformed characters from config parsing; wildcard patterns with '*' in the wrong position (only MaskDomain supports '*').
Related errors
- Scheme not supported
- Incorrect route name
- Incorrect route name
- path should be started with / or be empty
- 1007
AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11).
Data as JSON: /api/errors/d69f2c035646e067.
Report an issue: GitHub.
Appendix: source
Thrown at aiohttp/web_urldispatcher.py:790
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
def get_info(self) -> _InfoDict:
return {"domain": self._domain}
class MaskDomain(Domain):View on GitHub (pinned to d041d4d0fd)