春江暮客

春江暮客的个人学习分享网站

Troubleshoot Hostname Resolution with Python: getaddrinfo, IPv4/IPv6, and Wait Budgets

2026-10-19 Technology
Troubleshoot Hostname Resolution with Python: getaddrinfo, IPv4/IPv6, and Wait Budgets

A background task reports Temporary failure in name resolution. Increasing HTTP retries may only extend the wait. This tutorial builds a separate system hostname-resolution check with the Python standard library: IPv4 and IPv6 results, classified errors, exit codes, and an automation entry point with a wait budget.

It suits beginner-to-intermediate readers and follows the Python TCP port probe. Resolve the name first, then check the port and application response.

1. Method: inspect system resolution

The official socket.getaddrinfo() documentation describes its system-call wrapper. Our tool requests TCP candidates, deduplicates them in returned order, and retains IPv6 scope_id. It makes no connection.

On Linux with glibc, NSS configuration helps determine name sources and lookup order. A result can come from /etc/hosts, a cache, or DNS. Success does not prove that a DNS server was contacted during this run.

Use the Linux getaddrinfo error reference to distinguish:

status Meaning and next action
ok At least one address was returned; continue with connection checks.
temporary_failure EAI_AGAIN; investigate the resolution environment before a bounded retry.
name_or_service_unknown EAI_NONAME; check spelling, name sources, and address family. This is not evidence of a DNS NXDOMAIN response.
resolver_error Another resolver error; retain its code and message.
system_error Another system failure; investigate the reported code.
no_addresses No usable addresses; treat as failure.

2. Setup: no third-party packages

Use Linux and Python 3.9 or newer. Examples were tested on Linux with Python 3.11.4 and change no resolver configuration.

  1. Create a demonstration directory:
python3 --version
mkdir dns-check-demo
cd dns-check-demo
  1. Save the complete implementation below as check_dns.py. Input is an ASCII hostname such as localhost or www.bobobk.com; a trailing dot is allowed. URLs, port suffixes, and IPv6 literals are rejected. For an internationalized domain, use its ASCII/Punycode name first.

3. Implementation: one JSON line and explicit exit codes

"""Inspect system hostname resolution, without connecting. Python 3.9+."""
import argparse
import json
import re
import socket
import time

FAMILIES = {"any": socket.AF_UNSPEC, "ipv4": socket.AF_INET,
            "ipv6": socket.AF_INET6}


def hostname(value):
    name = value[:-1] if value.endswith(".") else value
    label = r"[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?"
    if len(name) > 253 or not re.fullmatch(label + r"(?:\." + label + r")*", name):
        raise argparse.ArgumentTypeError("use an ASCII hostname, not a URL")
    return value


def resolve(host, port, family):
    report = {"host": host, "port": port, "family": family,
              "status": "ok", "error_code": None, "error": "", "addresses": []}
    started = time.monotonic()
    try:
        rows = socket.getaddrinfo(host, port, FAMILIES[family],
                                  socket.SOCK_STREAM, socket.IPPROTO_TCP)
        seen = set()
        for af, _, _, _, address in rows:
            if af not in (socket.AF_INET, socket.AF_INET6):
                continue
            scope = address[3] if af == socket.AF_INET6 else 0
            key = (af, address[0], scope)
            if key in seen:
                continue
            seen.add(key)
            report["addresses"].append({
                "family": "ipv4" if af == socket.AF_INET else "ipv6",
                "address": address[0], "scope_id": scope,
            })
        if not report["addresses"]:
            report["status"] = "no_addresses"
    except socket.gaierror as exc:
        status = {socket.EAI_AGAIN: "temporary_failure",
                  socket.EAI_NONAME: "name_or_service_unknown"}.get(
                      exc.errno, "resolver_error")
        report.update(status=status, error_code=exc.errno, error=str(exc))
    except OSError as exc:
        report.update(status="system_error", error_code=exc.errno, error=str(exc))
    report["elapsed_ms"] = round((time.monotonic() - started) * 1000, 3)
    return report


def main():
    parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
    parser.add_argument("host", type=hostname)
    parser.add_argument("--port", type=int, default=443)
    parser.add_argument("--family", choices=FAMILIES, default="any")
    args = parser.parse_args()
    if not 1 <= args.port <= 65535:
        parser.error("--port must be between 1 and 65535")
    report = resolve(args.host, args.port, args.family)
    print(json.dumps(report, ensure_ascii=True))
    return 0 if report["status"] == "ok" else 1


if __name__ == "__main__":
    raise SystemExit(main())

--port supplies the numeric port for candidate socket addresses; it does not connect to that port. Interpret error_code with status: it can represent an EAI_* code or a system errno. Do not hard-code platform-specific error numbers.

4. Usage: local first, then the failing environment

  1. Read the help and resolve local IPv4 addresses:
python3 check_dns.py --help
python3 check_dns.py localhost --family ipv4

A typical result follows. Addresses and elapsed time depend on your machine:

{"host":"localhost","port":443,"family":"ipv4","status":"ok","error_code":null,"error":"","addresses":[{"family":"ipv4","address":"127.0.0.1","scope_id":0}],"elapsed_ms":0.2}
  1. Check the actual hostname inside the failing host or container. This site is an example:
python3 check_dns.py www.bobobk.com
python3 check_dns.py www.bobobk.com --family ipv4
python3 check_dns.py www.bobobk.com --family ipv6

The final command may fail when there is no IPv6 record, IPv6 support is unavailable, or the address family is restricted. That alone does not establish an IPv4 service failure. Records can change; do not hard-code this site’s IP in tests.

Exit 0 means addresses were returned, 1 means the check failed, and 2 means invalid arguments. After resolution succeeds, use curl to check HTTP status and response content.

5. Automation: bound the wait with a child process

getaddrinfo() has no timeout argument exposed by this tool. socket.setdefaulttimeout() is not its resolution timeout control. The observed elapsed_ms is not a deadline.

  1. Save this as run_check.py beside the resolver script:
"""Run check_dns.py in a child process with a five-second wait budget."""
import json
from pathlib import Path
import subprocess
import sys

script = Path(__file__).resolve().with_name("check_dns.py")
try:
    result = subprocess.run(
        [sys.executable, str(script), *sys.argv[1:]],
        capture_output=True, text=True, timeout=5, check=False,
    )
except subprocess.TimeoutExpired:
    print(json.dumps({"status": "wrapper_timeout", "wait_budget_seconds": 5}))
    raise SystemExit(124)
sys.stdout.write(result.stdout)
sys.stderr.write(result.stderr)
raise SystemExit(result.returncode)

The subprocess.run(timeout=...) documentation states that an expired timeout kills and waits for the child. This wrapper keeps resolution in that child, reports wrapper_timeout, and exits 124 when the wait budget expires. This differs from the resolver returning temporary_failure. Process creation and termination can add time, so this is not a guarantee to return within exactly five seconds.

  1. Run from the same directory. Save the exit code before printing a message to preserve failure:
if python3 run_check.py localhost --family ipv4; then
  printf 'Resolution passed; continue with connection checks\n'
else
  dns_exit=$?
  printf 'Resolution check failed: exit=%s\n' "$dns_exit" >&2
  exit "$dns_exit"
fi

For scheduled execution, use absolute paths to the actual interpreter and run_check.py. Follow the cron path and environment checks for the account, working directory, and container environment. A preflight is an observation at that moment; the subsequent client must still handle its own resolution and connection errors.

6. Validation: no public outage required

Save this as test_dns.py. The localhost case uses real system resolution. Injected data covers dual-stack results, duplicates, temporary failures, and other errors, without changing DNS, disconnecting networking, or waiting for a real timeout.

import errno
import json
from pathlib import Path
import socket
import subprocess
import sys
from unittest.mock import patch
import check_dns

root = Path(__file__).resolve().parent


def run(*args):
    return subprocess.run([sys.executable, str(root / "check_dns.py"), *args],
                          capture_output=True, text=True, timeout=5)


# Actual system resolution; no public hostname or remote connection required.
result = run("localhost", "--family", "ipv4")
report = json.loads(result.stdout)
assert result.returncode == 0 and report["status"] == "ok", report
assert report["addresses"] and report["elapsed_ms"] >= 0
assert all(item["family"] == "ipv4" for item in report["addresses"])

rows = [
    (socket.AF_INET, socket.SOCK_STREAM, 6, "", ("192.0.2.10", 443)),
    (socket.AF_INET, socket.SOCK_STREAM, 6, "", ("192.0.2.10", 443)),
    (socket.AF_INET6, socket.SOCK_STREAM, 6, "", ("2001:db8::10", 443, 0, 0)),
]
with patch("check_dns.socket.getaddrinfo", return_value=rows) as lookup:
    report = check_dns.resolve("example.test", 443, "any")
    assert report["status"] == "ok" and len(report["addresses"]) == 2, report
    assert [item["family"] for item in report["addresses"]] == ["ipv4", "ipv6"]
    lookup.assert_called_once_with("example.test", 443, socket.AF_UNSPEC,
                                   socket.SOCK_STREAM, socket.IPPROTO_TCP)

for failure, expected in [
    (socket.gaierror(socket.EAI_AGAIN, "injected temporary failure"), "temporary_failure"),
    (socket.gaierror(socket.EAI_NONAME, "injected unknown name"), "name_or_service_unknown"),
    (socket.gaierror(socket.EAI_FAIL, "injected failure"), "resolver_error"),
    (OSError(errno.EACCES, "injected denial"), "system_error"),
]:
    with patch("check_dns.socket.getaddrinfo", side_effect=failure):
        report = check_dns.resolve("example.test", 443, "any")
        assert report["status"] == expected and report["addresses"] == [], report
        assert report["error_code"] == failure.errno, report

with patch("check_dns.socket.getaddrinfo", return_value=[]):
    assert check_dns.resolve("example.test", 443, "any")["status"] == "no_addresses"

for arguments in [("https://example.com",), ("bad host",), ("",),
                  ("localhost", "--port", "0"), ("localhost", "--port", "65536"),
                  ("localhost", "--family", "wrong")]:
    result = run(*arguments)
    assert result.returncode == 2 and not result.stdout and result.stderr, result

print("OK: localhost, dual stack, deduplication, resolver errors, empty result, CLI errors")
python3 test_dns.py
python3 run_check.py localhost --family ipv4

The test script should print:

OK: localhost, dual stack, deduplication, resolver errors, empty result, CLI errors

Injected errors validate classification, not which error a real DNS server will produce. If the local case fails, inspect system resolution for localhost rather than changing the test to ignore failure.

7. Troubleshooting: inspect the caller’s environment

On glibc-based Linux with getent already available, compare these read-only observations:

getent ahosts localhost
getent ahosts www.bobobk.com
cat /etc/nsswitch.conf
cat /etc/resolv.conf

The getent ahosts manual describes its use of getaddrinfo(). Its flags differ from this tool’s, so counts and ordering need not match. Python checks still work when getent is unavailable.

Observation Concrete action
temporary_failure Compare resolver configuration and network reachability inside the same container; record the error before deciding on bounded retries.
name_or_service_unknown Check spelling, remove https:// and paths, and check IPv4 and IPv6 separately.
Host succeeds, container fails Compare the container’s own /etc/resolv.conf, NSS, and hosts sources; host results cannot substitute for container results.
Resolution succeeds, connection fails Continue with TCP, TLS, and HTTP checks; an address list does not establish service health.
wrapper_timeout Investigate resolution waits and process state; preserve exit 124 instead of emitting a false success.

8. Summary

A separate hostname check helps locate failures before or after connection setup. Keep observations in JSON and exit codes, then control automation waits through a child process. Continue with port and response-content checks after resolution succeeds to determine service availability.

标签

1024 ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation background backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn checkpoint cli cloudflare codex cofoldarena copy cpu monitoring cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization datetime debugging decimal deployment desktop-app devtools disk-space disown dns docker dovecot download economics electron encoding esm esm2 esm3 esmc esmfold2 fable faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io flashppi flask folium frontend game generator git github-actions google google-research grep gzip harness hls html http hugo imports indexnow javascript jev json jsonl just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp mirror mmcif model-evaluation monitoring mp3 mp4 mpnn multiomics mutation mysql nanobert nanobodies networkx nginx normalize numpy ollama omegatherm pandas password pathlib pdb pep-723 phaser pillow pip postfix preprocessing print probability protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security seo sequence-identity sha256 shell signals sklearn socket solana somaticsignatures spl sqlite ssh standardize static-site subprocess symlink sysadmin system-one tarfile tcp tensorflow tkinter tls toml trade tron tronpy troubleshooting turtle typesafe-ai ubuntu unicode usdt utf-8 uv vhhbert vite wallet webp wordcloud wordpress workflow yaml zipfile zoneinfo 迅雷解析

友情链接

其它