Isolate user-callback exceptions in process_packet#1762
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1762 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 26 26
Lines 4180 4183 +3
=========================================
+ Hits 4180 4183 +3 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
This might have a performance impact on the Bluetooth advertisement callback path. Prove if it does |
|
@esphbot Rr |
Rebase with requested adjustmentsBranch StatsActions performed
CI statusCI will be checked asynchronously. Automated by Kōan |
|
@esphbot Rr |
CodSpeed has now reported: 0 of 56 benchmarks changed on this branch (https://codspeed.io/esphome/aioesphomeapi/branches/esphbot%3Akoan%2Ffix-issue-1755), so the dispatch-path perf is unchanged on the Bluetooth advertisement hot path. A note on why this is cheaper than the per-site wrapper benchmarked in #1757: that benchmark wrapped the handler in an additional Python function whose body was |
PR Review — Isolate user-callback exceptions in process_packetClean, well-scoped fix for #1755. The try/except is placed at the right layer (inside Checklist
SummaryClean, well-scoped fix for #1755. The try/except is placed at the right layer (inside Automated review by Kōanedc86be |
A buggy registered handler used to propagate through process_packet into the frame helper's data_received, where asyncio treats the unwind as a transport error and calls connection_lost — tearing the whole ESPHome session down. The codebase already hardened the GATT-notify site (the only one) for exactly this reason. Wrap the per-handler dispatch in process_packet itself so every callback path is uniformly isolated, and drop the now-redundant per-site guard in on_bluetooth_gatt_notify_data_response. Internal protocol handlers (PingRequest, GetTimeRequest, DisconnectRequest) already report fatal errors via send_messages before raising, so swallowing the propagated exception doesn't lose teardown. Closes esphome#1755.
Rebase with requested adjustmentsBranch StatsActions performed
CI statusCI will be checked asynchronously. Automated by Kōan |
edc86be to
b959a11
Compare
There was a problem hiding this comment.
Pull request overview
This PR improves connection robustness by preventing exceptions raised inside registered (user) message handlers from unwinding through asyncio’s data_received path and tearing down the transport, aligning behavior across all callback sites.
Changes:
- Wrap per-handler dispatch in
APIConnection.process_packetwithtry/exceptto isolate callback exceptions and keep the session alive. - Remove the now-redundant exception guard in
on_bluetooth_gatt_notify_data_response, relying on the centralized isolation. - Add/adjust tests to assert that callback exceptions are logged, do not disconnect the connection, and do not prevent sibling handlers from running.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
aioesphomeapi/connection.py |
Centralizes exception isolation around message handler dispatch within process_packet. |
aioesphomeapi/client_base.py |
Removes per-site try/except for Bluetooth GATT notify callback since process_packet now isolates handlers. |
tests/test_connection.py |
Adds regression tests ensuring handler exceptions don’t disconnect and don’t skip other handlers. |
tests/test_client.py |
Updates expectations for Bluetooth notify exception logging to match the new centralized log message. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| _LOGGER.exception( | ||
| "%s: Unexpected error in message handler %r for %s", | ||
| self.log_name, | ||
| handler, | ||
| type(msg).__name__, | ||
| ) |
| _LOGGER.exception( | ||
| "%s: Unexpected error in message handler %r for %s", | ||
| self.log_name, | ||
| handler, | ||
| type(msg).__name__, | ||
| ) |
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
WalkthroughHandler exceptions in ChangesHandler Exception Isolation via Central Dispatcher
🎯 2 (Simple) | ⏱️ ~12 minutes ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
What does this implement/fix?
APIConnection.process_packetused to invoke registered handlerswithout any exception isolation. If a user callback raised, the
exception unwound through the frame helper's
data_receivedintoasyncio, which treats it as a transport error and calls
connection_lost— tearing the whole ESPHome session down. Thecodebase was aware of the trap and hardened the GATT-notify site
(the only one) for exactly this reason.
This wraps the per-handler dispatch in
process_packetitself soevery callback path is uniformly isolated, and drops the now-
redundant per-site guard in
on_bluetooth_gatt_notify_data_response. Internal protocolhandlers (
PingRequest,GetTimeRequest,DisconnectRequest)already call
report_fatal_errorviasend_messagesbeforeraising, so swallowing the propagated exception still triggers
the orderly teardown path.
Benchmarks for the dispatch path were added separately in #1757 to
let the maintainer measure the cost; the upper-bound per-site
numbers (Python try/except + extra frame) were within noise for
typical state messages and ~15% on wide-state climate updates.
Wrapping inside
process_packet(this PR) avoids the extra Pythonframe, so the actual cost will be lower than that upper bound; on
the Cython hot path Cython's try/except is also cheaper than
Python's.
Types of changes
Related issue or feature (if applicable):
Pull request in esphome (if applicable):
Checklist:
tests/folder).Test plan
tests/test_connection.py::test_handler_exception_does_not_disconnect— raising user callback is logged but the connection stays up.tests/test_connection.py::test_handler_exception_does_not_skip_siblings— when one of several handlers raises, the others still run on the same message.tests/test_client.py::test_bluetooth_gatt_notify_callback_raises— updated to the new central log message; still pins the original property (callback exception caught, connection alive).1160 passed, 1 skipped.Generated by Kōan
Quality Report
Changes: 4 files changed, 81 insertions(+), 12 deletions(-)
Code scan: clean
Tests: failed ([Errno 13] Permission denied: 'pytest')
Branch hygiene: clean
Generated by Kōan post-mission quality pipeline