Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion aiodns/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,13 @@ def _query_callback(
fut.set_exception(
error.DNSError(errorno, pycares.errno.strerror(errorno))
)
return
try:
converted = convert_result(result, qtype)
except error.DNSError as exc:
fut.set_exception(exc)
else:
fut.set_result(convert_result(result, qtype))
fut.set_result(converted)

def _get_query_future_callback(
self, qtype: int
Expand Down
26 changes: 21 additions & 5 deletions aiodns/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@

import pycares

from . import error

_SINGLE_RESULT_QTYPES = frozenset(
{
pycares.QUERY_TYPE_CNAME,
pycares.QUERY_TYPE_SOA,
pycares.QUERY_TYPE_PTR,
}
)


def _maybe_str(data: bytes) -> str | bytes:
"""Decode bytes as ASCII, return bytes if decode fails (pycares 4.x)."""
Expand Down Expand Up @@ -260,13 +270,19 @@ def convert_result(dns_result: pycares.DNSResult, qtype: int) -> QueryResult:
converted = _convert_record(record)

# CNAME, SOA, and PTR return single result, not list
if record_type in (
pycares.QUERY_TYPE_CNAME,
pycares.QUERY_TYPE_SOA,
pycares.QUERY_TYPE_PTR,
):
if record_type in _SINGLE_RESULT_QTYPES:
return cast(QueryResult, converted)

results.append(converted)

# NOERROR/NODATA: c-ares delivered ARES_SUCCESS but the answer has no
# records of the queried type. pycares 4.x raised ARES_ENODATA here;
# without this branch single-result qtypes (CNAME/SOA/PTR) would
# resolve to [] and crash callers reading .name/.cname/.nsname.
if not results:
raise error.DNSError(
pycares.errno.ARES_ENODATA,
pycares.errno.strerror(pycares.errno.ARES_ENODATA),
)

return results
18 changes: 18 additions & 0 deletions tests/test_aiodns.py
Original file line number Diff line number Diff line change
Expand Up @@ -1398,6 +1398,24 @@ async def test_query_callback_error() -> None:
resolver._closed = True


@pytest.mark.asyncio
async def test_query_callback_empty_result_raises_enodata() -> None:
"""convert_result raising must route through fut.set_exception."""
resolver = aiodns.DNSResolver(timeout=5.0)
fut: asyncio.Future[Any] = asyncio.get_event_loop().create_future()

empty = unittest.mock.MagicMock(spec=pycares.DNSResult)
empty.answer = []

resolver._query_callback(fut, pycares.QUERY_TYPE_PTR, empty, None)

with pytest.raises(aiodns.error.DNSError) as exc_info:
fut.result()
assert exc_info.value.args[0] == pycares.errno.ARES_ENODATA

resolver._closed = True


async def _assert_malformed_name_routes_through_future(
fut: asyncio.Future[Any],
) -> None:
Expand Down
51 changes: 46 additions & 5 deletions tests/test_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import pycares
import pytest

from aiodns import error
from aiodns.compat import (
AresHostResult,
AresQueryAAAAResult,
Expand Down Expand Up @@ -506,11 +507,51 @@ def test_convert_any_query_returns_all_records(self) -> None:
assert isinstance(result[0], AresQueryAResult)
assert isinstance(result[1], AresQueryMXResult)

def test_convert_empty_result(self) -> None:
"""Test conversion of empty DNS result."""
@pytest.mark.parametrize(
'qtype',
[
pycares.QUERY_TYPE_A,
pycares.QUERY_TYPE_AAAA,
pycares.QUERY_TYPE_CAA,
pycares.QUERY_TYPE_CNAME,
pycares.QUERY_TYPE_MX,
pycares.QUERY_TYPE_NAPTR,
pycares.QUERY_TYPE_NS,
pycares.QUERY_TYPE_PTR,
pycares.QUERY_TYPE_SOA,
pycares.QUERY_TYPE_SRV,
pycares.QUERY_TYPE_TXT,
],
)
def test_convert_empty_result_raises_enodata(self, qtype: int) -> None:
"""NOERROR/NODATA must raise ARES_ENODATA; see aiodns_bug.md."""
dns_result = make_mock_dns_result([])

result = convert_result(dns_result, pycares.QUERY_TYPE_A)
with pytest.raises(error.DNSError) as exc_info:
convert_result(dns_result, qtype)

assert isinstance(result, list)
assert len(result) == 0
assert exc_info.value.args[0] == pycares.errno.ARES_ENODATA

def test_convert_ptr_with_only_non_ptr_records_raises_enodata(
self,
) -> None:
"""PTR query whose answer carries only a CNAME chain must raise."""
cname_data = unittest.mock.MagicMock()
cname_data.cname = 'alias.example.com'
records = [
make_mock_record(pycares.QUERY_TYPE_CNAME, cname_data, ttl=300),
]
dns_result = make_mock_dns_result(records)

with pytest.raises(error.DNSError) as exc_info:
convert_result(dns_result, pycares.QUERY_TYPE_PTR)

assert exc_info.value.args[0] == pycares.errno.ARES_ENODATA

def test_convert_any_empty_result_returns_empty_list(self) -> None:
"""ANY is always list-shaped, so empty stays empty (no raise)."""
dns_result = make_mock_dns_result([])

result = convert_result(dns_result, pycares.QUERY_TYPE_ANY)

assert result == []
Loading