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

  1. Normalize the host to dotted-quad with `ipaddress.ip_address(host).exploded` / socket.inet_ntoa before building the URL.
  2. Reject or rewrite non-canonical IPv4 inputs at your URL validation boundary.
  3. 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

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


AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04). Data as JSON: /data/errors/fec920ba5e999b6f.json. Report an issue: GitHub.