Python interface for c-ares asynchronous DNS library
—
Comprehensive error handling for DNS operations including error codes, utility functions, and debugging support. All pycares operations use consistent error reporting through callback functions and exception classes.
DNS resolution can fail for various reasons. pycares provides detailed error codes to help diagnose and handle different failure scenarios.
# Success code
ARES_SUCCESS = 0
# DNS-specific errors
ARES_ENODATA = 1 # No data in DNS response
ARES_EFORMERR = 2 # DNS format error
ARES_ESERVFAIL = 3 # DNS server failure
ARES_ENOTFOUND = 4 # Domain name not found
ARES_ENOTIMP = 5 # Query type not implemented
ARES_EREFUSED = 6 # DNS query refused by server
ARES_EBADQUERY = 7 # Malformed DNS query
ARES_EBADNAME = 8 # Malformed domain name
ARES_EBADFAMILY = 9 # Bad address family
ARES_EBADRESP = 10 # Bad DNS response
ARES_ECONNREFUSED = 11 # Connection refused
ARES_ETIMEOUT = 12 # Query timeout
ARES_EOF = 13 # End of file
ARES_EFILE = 14 # File I/O error
ARES_ENOMEM = 15 # Out of memory
ARES_EDESTRUCTION = 16 # Channel destroyed
ARES_EBADSTR = 17 # Bad string
ARES_EBADFLAGS = 18 # Bad flags
ARES_ENONAME = 19 # No name
ARES_EBADHINTS = 20 # Bad hints
ARES_ENOTINITIALIZED = 21 # Not initialized
ARES_ELOADIPHLPAPI = 22 # Failed to load iphlpapi
ARES_EADDRGETNETWORKPARAMS = 23 # Failed to get network parameters
ARES_ECANCELLED = 24 # Query cancelled
ARES_ESERVICE = 25 # Service errorMapping dictionary for converting error codes to symbolic names.
errorcode: dict[int, str] # Maps error codes to string namesUsage Example:
import pycares.errno
# Print all available error codes
for code, name in pycares.errno.errorcode.items():
print(f'{code}: {name}')
# Check if an error code is known
error_code = 4
if error_code in pycares.errno.errorcode:
print(f'Error {error_code} is {pycares.errno.errorcode[error_code]}')Convert error codes to human-readable error messages.
def strerror(code: int) -> str:
"""
Get human-readable error message for an error code.
Args:
code: int - Error code from callback or exception
Returns:
str: Human-readable error message
"""Usage Example:
import pycares
import pycares.errno
def query_callback(result, error):
if error is not None:
error_name = pycares.errno.errorcode.get(error, f'UNKNOWN_{error}')
error_msg = pycares.errno.strerror(error)
print(f'DNS query failed: {error_name} - {error_msg}')
return
print(f'Query successful: {result}')
channel = pycares.Channel()
channel.query('nonexistent.invalid', pycares.QUERY_TYPE_A, query_callback)Primary exception class for c-ares related errors.
class AresError(Exception):
"""
Exception raised for c-ares library errors.
This exception is raised for synchronous errors during channel creation,
configuration, or other immediate operations. Asynchronous query errors
are reported via callback functions, not exceptions.
"""Usage Example:
import pycares
try:
# This might raise AresError if initialization fails
channel = pycares.Channel(servers=['invalid.ip.address'])
except pycares.AresError as e:
print(f'Failed to create channel: {e}')
try:
channel = pycares.Channel()
# This might raise AresError if reinit fails
channel.reinit()
except pycares.AresError as e:
print(f'Failed to reinitialize channel: {e}')import pycares
import pycares.errno
def handle_dns_result(result, error):
"""Standard DNS result handler with error checking."""
if error is not None:
if error == pycares.errno.ARES_ENOTFOUND:
print('Domain not found')
elif error == pycares.errno.ARES_ETIMEOUT:
print('Query timed out')
elif error == pycares.errno.ARES_ESERVFAIL:
print('DNS server failure')
else:
print(f'DNS error: {pycares.errno.strerror(error)}')
return
# Process successful result
print(f'DNS query successful: {result}')
channel = pycares.Channel()
channel.query('example.com', pycares.QUERY_TYPE_A, handle_dns_result)class DNSResolver:
def __init__(self, max_retries=3):
self.channel = pycares.Channel(timeout=5.0, tries=2)
self.max_retries = max_retries
def query_with_retry(self, name, query_type, callback, retries=0):
def retry_callback(result, error):
if error is not None and retries < self.max_retries:
# Retry on timeout or server failure
if error in (pycares.errno.ARES_ETIMEOUT, pycares.errno.ARES_ESERVFAIL):
print(f'Retrying query ({retries + 1}/{self.max_retries})')
self.query_with_retry(name, query_type, callback, retries + 1)
return
# Final result (success or non-retryable error)
callback(result, error)
self.channel.query(name, query_type, retry_callback)
resolver = DNSResolver()
resolver.query_with_retry('google.com', pycares.QUERY_TYPE_A, handle_dns_result)import pycares.errno
def categorize_error(error_code):
"""Categorize DNS errors for appropriate handling."""
if error_code == pycares.errno.ARES_SUCCESS:
return 'success'
elif error_code in (pycares.errno.ARES_ENOTFOUND, pycares.errno.ARES_ENODATA):
return 'not_found'
elif error_code in (pycares.errno.ARES_ETIMEOUT, pycares.errno.ARES_ECONNREFUSED):
return 'network'
elif error_code in (pycares.errno.ARES_ESERVFAIL, pycares.errno.ARES_EREFUSED):
return 'server'
elif error_code in (pycares.errno.ARES_EBADNAME, pycares.errno.ARES_EBADQUERY):
return 'invalid_input'
elif error_code == pycares.errno.ARES_ECANCELLED:
return 'cancelled'
else:
return 'unknown'
def handle_categorized_error(result, error):
if error is None:
print(f'Success: {result}')
return
category = categorize_error(error)
error_msg = pycares.errno.strerror(error)
if category == 'not_found':
print(f'Domain not found: {error_msg}')
elif category == 'network':
print(f'Network error (check connectivity): {error_msg}')
elif category == 'server':
print(f'DNS server error (try different server): {error_msg}')
elif category == 'invalid_input':
print(f'Invalid input (check domain name): {error_msg}')
elif category == 'cancelled':
print('Query was cancelled')
else:
print(f'Unknown error: {error_msg}')import time
import threading
import pycares
class TimeoutHandler:
def __init__(self, channel, timeout_seconds=10):
self.channel = channel
self.timeout_seconds = timeout_seconds
self.active_queries = set()
def query_with_timeout(self, name, query_type, callback):
query_id = id(callback) # Simple query identifier
self.active_queries.add(query_id)
# Start timeout timer
def timeout_callback():
if query_id in self.active_queries:
self.active_queries.remove(query_id)
callback(None, pycares.errno.ARES_ETIMEOUT)
timer = threading.Timer(self.timeout_seconds, timeout_callback)
timer.start()
# Wrap original callback
def wrapped_callback(result, error):
if query_id in self.active_queries:
timer.cancel()
self.active_queries.remove(query_id)
callback(result, error)
self.channel.query(name, query_type, wrapped_callback)
# Usage
handler = TimeoutHandler(pycares.Channel(), timeout_seconds=5)
handler.query_with_timeout('slow-server.com', pycares.QUERY_TYPE_A, handle_dns_result)import logging
import pycares.errno
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
def debug_callback(name, query_type):
"""Create a callback with debug logging."""
def callback(result, error):
if error is not None:
error_name = pycares.errno.errorcode.get(error, f'UNKNOWN_{error}')
error_msg = pycares.errno.strerror(error)
logger.error(f'Query failed: {name} ({query_type}) - {error_name}: {error_msg}')
else:
logger.info(f'Query successful: {name} ({query_type}) - {len(result) if isinstance(result, list) else 1} records')
logger.debug(f'Result: {result}')
return callback
channel = pycares.Channel()
channel.query('google.com', pycares.QUERY_TYPE_A, debug_callback('google.com', 'A'))class ErrorStats:
def __init__(self):
self.error_counts = {}
self.success_count = 0
def record_result(self, error):
if error is None:
self.success_count += 1
else:
error_name = pycares.errno.errorcode.get(error, f'UNKNOWN_{error}')
self.error_counts[error_name] = self.error_counts.get(error_name, 0) + 1
def print_stats(self):
total = self.success_count + sum(self.error_counts.values())
print(f'DNS Query Statistics (Total: {total})')
print(f' Successful: {self.success_count}')
for error_name, count in sorted(self.error_counts.items()):
print(f' {error_name}: {count}')
stats = ErrorStats()
def stats_callback(result, error):
stats.record_result(error)
if error is None:
print(f'Success: {result}')
else:
print(f'Error: {pycares.errno.strerror(error)}')
# After running queries...
stats.print_stats()# DNS server unreachable
# Error: ARES_ECONNREFUSED or ARES_ETIMEOUT
# Firewall blocking DNS
# Error: ARES_ETIMEOUT
# No internet connection
# Error: ARES_ECONNREFUSED or ARES_ETIMEOUT# Invalid DNS server configured
# Error: ARES_ESERVFAIL or ARES_ETIMEOUT
# DNS server not responding
# Error: ARES_ETIMEOUT
# DNS server refusing queries
# Error: ARES_EREFUSED# Domain doesn't exist
# Error: ARES_ENOTFOUND
# Record type doesn't exist for domain
# Error: ARES_ENODATA
# Malformed domain name
# Error: ARES_EBADNAME
# Invalid query parameters
# Error: ARES_EBADQUERYAll error handling in pycares follows the pattern of passing error codes to callback functions rather than raising exceptions for asynchronous operations. Synchronous configuration errors raise AresError exceptions.
Install with Tessl CLI
npx tessl i tessl/pypi-pycares