Route synchronous pycares AresError through the returned future#245
Conversation
When pycares raises AresError synchronously (e.g. ARES_EBADNAME for a malformed hostname), the future was created inside the resolver method but never returned to the caller, leaving an orphaned future with an unretrieved exception and producing a noisy "Future exception was never retrieved" warning. Wrap each pycares call with a `_capture_ares_error` context manager that sets the error on the future and lets it surface from `await`, matching the behavior of asynchronous c-ares errors. Fixes #231
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #245 +/- ##
==========================================
+ Coverage 98.36% 98.42% +0.06%
==========================================
Files 5 5
Lines 1345 1399 +54
Branches 70 73 +3
==========================================
+ Hits 1323 1377 +54
Misses 16 16
Partials 6 6 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
…ated The `@contextlib.contextmanager` decorator combined with `asyncio.Future[Any]` made the wrapped callable's type contain Any, which mypy's disallow_any_decorated rejects. Parameterize the future with the existing _T TypeVar to keep the contract polymorphic over the future's payload type without leaking Any into the signature.
Split the original parametrize into two focused integration tests sharing a small assertion helper, and add a direct unit test for _capture_ares_error that exercises the `if fut.done(): return` path by driving the context manager with __enter__/__exit__ on an already resolved future.
|
@bluetoothbot review |
There was a problem hiding this comment.
Pull request overview
This PR fixes an aiodns behavior where pycares.AresError could be raised synchronously (e.g., malformed hostnames), leaving an internally-created Future orphaned and emitting Future exception was never retrieved. The change ensures synchronous c-ares errors are routed into the returned Future, so callers consistently see failures via await.
Changes:
- Add a
_capture_ares_error()context manager to convert synchronouspycares.AresErrorintoaiodns.error.DNSErroron the created future. - Wrap
pycares.Channelcalls (query,query_dns,getaddrinfo, etc.) so synchronous errors complete the future instead of bubbling synchronously. - Add regression tests covering malformed-name behavior and a done-future edge case.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
aiodns/__init__.py |
Wraps synchronous pycares calls to ensure errors are surfaced via the returned future; adjusts callbacks to bail out on fut.done(). |
tests/test_aiodns.py |
Adds regression tests to ensure malformed hostnames raise through await and that done futures aren’t overridden. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Address Copilot review feedback on #245: * Handle pycares.AresError instances with an unexpected args shape: fall through quietly when args is empty and derive the message from pycares.errno.strerror(errno) instead of trusting args[1], matching the behavior of _callback / _query_callback. * Drive the done-future unit test through a real `with` block, so the scenario being exercised mirrors how the context manager is actually used in production code.
If the `if fut.done(): return` short-circuit doesn't fire, the context manager body would call set_exception() on a done future, raising InvalidStateError. Just running the `with raise exc` block to completion is therefore a sufficient assertion, and it keeps the test free of the extra try/except scaffolding that was only there to avoid the unreachable-statement warning.
PR Review — Route synchronous pycares AresError through the returned futureCorrect, narrowly-scoped fix for #231. The 🟢 Suggestions1. Empty AresError.args silently hangs the future (`aiodns/__init__.py`, L229-234)When Something like: except pycares.AresError as exc:
if fut.done():
return
errno = exc.args[0] if exc.args else pycares.errno.ARES_EFORMERR
fut.set_exception(
error.DNSError(errno, pycares.errno.strerror(errno))
)2. Comment why _callback now guards on fut.done() instead of fut.cancelled() (`aiodns/__init__.py`, L161-195)The switch from 3. Make the done-future test assertion explicit (`tests/test_aiodns.py`, L1438-1450)
4. Add coverage for the other wrapped entry points (`tests/test_aiodns.py`, L1409-1424)
Checklist
SummaryCorrect, narrowly-scoped fix for #231. The Automated review by Kōanc5567de |
* _capture_ares_error: never silently leave the future unset; fall back to ARES_EFORMERR when AresError carries no args so callers always see a DNSError on `await` instead of an indefinite hang. * Document the load-bearing fut.done() guard in the three callbacks so a future cleanup does not regress them back to fut.cancelled(). * Cover the remaining wrapped entry points (getaddrinfo, getnameinfo, gethostbyaddr) by injecting a synchronous AresError via mock; pycares itself never raises here today, so a real-input regression test is not possible, but the mocked test prevents the wrapper from being silently removed in a future refactor.
The trailing AssertionError branch was unreachable given the parametrize values; switch the final check to an `assert channel_method == 'gethostbyaddr'` so the dispatch stays exhaustive without dead code.
Summary
Home Assistant was about to merge a workaround for this so I figured we should fix it here instead home-assistant/core#170048
Fixes #231; query methods could raise
pycares.AresErrorsynchronously (for example on a malformed hostname), leaving the internally created future orphaned with an unretrieved exception and producing a noisyFuture exception was never retrievedwarning.Each pycares call is now wrapped in a
_capture_ares_errorcontext manager that sets the error on the future, so callers can rely onawaitto raise just like for asynchronous c-ares errors.Test plan
tests/test_aiodns.pycoverquery,query_dns, and the exact example from the issue