odysseus-dev/odysseus · error · SkillImportError

outbound URL blocked: host did not resolve to a usable addre

Error message

outbound URL blocked: host did not resolve to a usable address

What it means

Raised in _resolve_and_check_url (services/memory/skill_importer.py) when check_outbound_url passed but none of the recorded DNS answers could be parsed into a usable ipaddress object (_validated_ips returned nothing). The importer pins every connection to the validated IP snapshot (via _PinnedBackend), so an empty/non-parseable answer set is fatal even though the URL itself looked acceptable.

Source

Thrown at services/memory/skill_importer.py:115

    """Return the exact address snapshot approved for one fetch hop."""
    resolved_ips: List[str] = []

    def _recording_resolver(host: str) -> List[str]:
        answers = list(_default_resolver(host))
        resolved_ips[:] = answers
        return answers

    ok, reason = check_outbound_url(
        url,
        block_private=True,
        resolver=_recording_resolver,
    )
    if not ok:
        raise SkillImportError(f"outbound URL blocked: {reason}")

    pinned_ips = _validated_ips(resolved_ips)
    if not pinned_ips:
        raise SkillImportError("outbound URL blocked: host did not resolve to a usable address")
    return pinned_ips


# Backward compatibility alias for tests importing _check_fetch_url directly
_check_fetch_url = _resolve_and_check_url


class _PinnedBackend(httpcore.NetworkBackend):
    """Connect only to addresses from one validated DNS snapshot."""

    def __init__(self, ips: List[ipaddress._BaseAddress]):
        self._ips = [str(ip) for ip in ips]
        self._real = httpcore.SyncBackend()

    def connect_tcp(
        self,
        host: str,
        port: int,

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Check what the host actually resolves to: dig +short <host> / python -c 'import socket; print(socket.getaddrinfo("<host>", 443))'.
  2. Fix or bypass the broken resolver (point /etc/resolv.conf at a working resolver, disconnect the VPN DNS).
  3. Retry the import — transient partial DNS answers can produce an empty validated set once and succeed next time.
  4. If the host genuinely has no A/AAAA record, the URL is dead; find the correct GitHub URL for the skill.

Example fix

# before
$ python -c "import socket; print(socket.getaddrinfo('raw.githubusercontent.com', 443))"
[]   # resolver returns nothing parseable
SkillImportError: outbound URL blocked: host did not resolve to a usable address

# after
$ sudo resolvectl flush-caches && dig +short raw.githubusercontent.com   # returns real A records
# retry the import
Defensive patterns

Strategy: retry

Validate before calling

import socket

def host_resolves_to_ips(host: str) -> list[str]:
    try:
        return list({info[4][0] for info in socket.getaddrinfo(host, 443, proto=socket.IPPROTO_TCP)})
    except socket.gaierror:
        return []

Type guard

def host_has_usable_address(host: str) -> bool:
    ips = host_resolves_to_ips(host)
    return bool(ips) and all(ip.count('.') == 3 or ':' in ip for ip in ips)

Try / catch

import time
from services.memory.skill_importer import SkillImportError

for attempt in range(3):
    try:
        import_skill(url)
        break
    except SkillImportError as e:
        if 'did not resolve' in str(e) and attempt < 2:
            time.sleep(2 * (attempt + 1))  # transient DNS; flush caches / check resolver if persistent
            continue
        raise

Prevention

When it happens

Trigger: A host whose DNS returns only record types the resolver surfaces as non-IP strings (bare CNAME chains, IPv6 addresses that fail ipaddress parsing after the '%' scope-strip, or resolver software returning nonstandard values); a recording resolver that returned an empty list the checker happened to tolerate; DNS64/NAT64 environments returning synthetic addresses in odd formats.

Common situations: Exotic or broken local resolvers (systemd-resolved in odd modes, VPN DNS plugins); hosts that are CNAME-only at the moment of resolution; transient DNS misconfiguration during import; test environments mocking DNS with non-IP strings.

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/b632043b0b838175. Report an issue: GitHub.