aio-libs/aiohttp · error · ValueError
Scheme not supported
Error message
Scheme not supported
What it means
Thrown by Domain.validation() when the domain string passed to Application.add_domain() (or a Domain/MatchDomain rule) contains '://'. aiohttp attaches sub-applications by host, not by full URL, so it prepends 'http://' internally to parse the host; an embedded scheme breaks that logic and is rejected. Pass a bare hostname like 'example.com' or 'example.com:8080' instead of 'http://example.com'.
Solutions
- Strip the scheme before passing the host: use URL(value).raw_host (and URL(value).port) instead of the raw string.
- Pass only the host[:port] form, e.g. 'example.com' or 'example.com:8443'.
- Sanitize env-derived config: host = value.split('://')[-1] before add_domain.
Example fix
// before
app.add_domain('https://api.example.com', sub_app)
// after
from yarl import URL
u = URL('https://api.example.com')
app.add_domain(f'{u.raw_host}:{u.port}' if u.port else u.raw_host, sub_app) Defensive patterns
Strategy: validation
Validate before calling
from yarl import URL
def normalize_domain(value: str) -> str:
if '://' in value:
u = URL(value)
host = u.raw_host
return f'{host}:{u.port}' if u.port and u.port != 80 else host
return value
# usage:
app.add_domain(normalize_domain(cfg_host), sub_app) Type guard
import re
DNS_LABEL = re.compile(r'(?!-)[a-z0-9*-]{1,63}(?<!-)', re.I)
def is_bare_host(value: str) -> bool:
return '://' not in value and all(
DNS_LABEL.fullmatch(p) for p in value.rstrip('.').lower().split('.') if p
) Try / catch
try:
app.add_domain(host_str, sub_app)
except ValueError as e:
if 'Scheme not supported' in str(e):
log.error('add_domain needs a bare host, got %r', host_str)
raise Prevention
- Always pass host[:port] to add_domain, never a full URL.
- Derive the host from config via URL(raw).raw_host rather than string manipulation.
- Unit-test add_domain inputs against a list of known-good and known-bad hosts.
When it happens
Trigger: Calling app.add_domain('https://api.example.com', sub_app) or constructing Domain('http://localhost:8080'). Any value containing '://' after rstrip('.').lower() hits line 786.
Common situations: Copy-pasting a full URL from a browser or config file into add_domain(); migrating from a framework that accepts full URLs; reading the host from an environment variable that includes the scheme (e.g. BASE_URL=https://api.example.com).
Related errors
- Domain not valid
- 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/f21cd9bf2b1f56c0.
Report an issue: GitHub.
Appendix: source
Thrown at aiohttp/web_urldispatcher.py:786
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)
def match_domain(self, host: str) -> bool:
return host.lower() == self._domain
def get_info(self) -> _InfoDict:View on GitHub (pinned to d041d4d0fd)