aio-libs/aiohttp · error · InvalidUrlClientError
{host} - is not a canonical IPv4 address
Error message
{host} - is not a canonical IPv4 address What it means
Raised by TCPConnector._resolve_host() when the URL host parses as an IP literal but is a legacy non-canonical IPv4 form (e.g. `2130706433`, `127.1`, `0x7f000001`). aiohttp now requires dotted-quad canonical form (`127.0.0.1`) because the legacy forms can slip past connector-level host policies. The check is `is_canonical_ipv4_address` in helpers.py.
Source
Thrown at aiohttp/connector.py:1131
def clear_dns_cache(self, host: str | None = None, port: int | None = None) -> None:
"""Remove specified host/port or clear all dns local cache."""
if host is not None and port is not None:
self._cached_hosts.remove((host, port))
elif host is not None or port is not None:
raise ValueError("either both host and port or none of them are allowed")
else:
self._cached_hosts.clear()
async def _resolve_host(
self, host: str, port: int, traces: Sequence["Trace"] | None = None
) -> list[ResolveResult]:
"""Resolve host and return list of addresses."""
if is_ip_address(host):
# Reject legacy numeric IPv4 forms (e.g. 2130706433, 127.1) that
# socket would map onto an address, slipping past a connector-level
# policy that only sees the raw host.
if ":" not in host and not is_canonical_ipv4_address(host):
raise InvalidUrlClientError(host, "is not a canonical IPv4 address")
return [
{
"hostname": host,
"host": host,
"port": port,
"family": self._family,
"proto": 0,
"flags": 0,
}
]
if not self._use_dns_cache:
if traces:
for trace in traces:
await trace.send_dns_resolvehost_start(host)
if self._closed:
raise ClientConnectionError("Connector is closed")View on GitHub (pinned to c0ef574e29)
Solutions
- Normalize the host to dotted-quad with `ipaddress.ip_address(host).exploded` / socket.inet_ntoa before building the URL.
- Reject or rewrite non-canonical IPv4 inputs at your URL validation boundary.
- If the integer is intentional, convert it explicitly: `socket.inet_ntoa(struct.pack('!I', int(host)))`.
Example fix
# before
url = 'http://2130706433/'
# after
import ipaddress
host = str(ipaddress.ip_address(2130706433)) # '127.0.0.1'
url = f'http://{host}/' Defensive patterns
Strategy: validation
Validate before calling
import ipaddress
def canonicalize_host(host: str) -> str:
try:
return str(ipaddress.ip_address(host))
except ValueError:
return host # leave DNS names alone Type guard
def is_canonical_ipv4(host: str) -> bool:
parts = host.split('.')
if len(parts) != 4:
return False
return all(p.isdigit() and 0 <= int(p) <= 255 and (p == '0' or not p.startswith('0')) for p in parts) Try / catch
from aiohttp import InvalidUrlClientError
try:
await session.get(url)
except InvalidUrlClientError as e:
if 'canonical IPv4' in str(e):
# normalize and retry
...
raise Prevention
- Validate user-supplied URLs at the trust boundary with urllib/yparser before passing to aiohttp.
- Normalize integer/compact IPv4 forms to dotted-quad explicitly when they are intentional.
- Log rejected hosts so obfuscated-input attempts are visible.
When it happens
Trigger: Requesting a URL whose host is `http://2130706433/` or `http://127.1/` - forms that socket would accept but aiohttp rejects, specifically when the host has no colon (IPv4 path) and fails the canonical check.
Common situations: User-supplied or scraped URLs using compact IPv4 notation. Obfuscated URLs / security research payloads. Configs that derive host from integer identifiers.
Related errors
- base_url must have a trailing '/'
- Cannot combine AUTHORIZATION header with credentials encoded
- Method cannot contain non-token characters {method!r} (found
- {url}
- keepalive_timeout cannot be set if force_close is True
AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04).
Data as JSON: /data/errors/fec920ba5e999b6f.json.
Report an issue: GitHub.