Skip to content

Commit c3db8e2

Browse files
fix: wrap aiohttp network errors in RoborockException (#835)
Network-class exceptions raised inside PreparedRequest.request (aiohttp.ClientError, TimeoutError, OSError) were bubbling out unwrapped, bypassing consumers that catch RoborockException. This caused Home Assistant's roborock integration to mark config entries as terminally failed after a power outage when DNS hadn't resolved yet, requiring a manual restart to recover. Catch the network-class exceptions at the single chokepoint (PreparedRequest.request) and re-raise as RoborockException, preserving the original cause via `from err`. Every method on RoborockApiClient / UserWebApiClient benefits since they all route through this method. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 81daad1 commit c3db8e2

2 files changed

Lines changed: 25 additions & 2 deletions

File tree

roborock/web_api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,8 @@ async def request(self, method: str, url: str, params=None, data=None, headers=N
692692
_LOGGER.info("Resp raw: %s", resp_raw)
693693
# Still raise the err so that it's clear it failed.
694694
raise err
695+
except (aiohttp.ClientError, TimeoutError, OSError) as err:
696+
raise RoborockException(f"Network error contacting {_url}: {err}") from err
695697
finally:
696698
if close_session:
697699
await session.close()

tests/test_web_api.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
from aioresponses.compat import normalize_url
88

99
from roborock import HomeData, HomeDataScene, UserData
10-
from roborock.exceptions import RoborockAccountDoesNotExist, RoborockInvalidCredentials
11-
from roborock.web_api import IotLoginInfo, RoborockApiClient, UserWebApiClient
10+
from roborock.exceptions import RoborockAccountDoesNotExist, RoborockException, RoborockInvalidCredentials
11+
from roborock.web_api import IotLoginInfo, PreparedRequest, RoborockApiClient, UserWebApiClient
1212
from tests.mock_data import HOME_DATA_RAW, USER_DATA
1313

1414
pytest_plugins = [
@@ -398,3 +398,24 @@ async def test_user_web_api_client_unauthorized_hook() -> None:
398398
with pytest.raises(RoborockInvalidCredentials):
399399
await client.get_rooms()
400400
mock_hook.assert_called_once()
401+
402+
403+
@pytest.mark.parametrize(
404+
"exception",
405+
[
406+
aiohttp.ClientError("connection failed"),
407+
TimeoutError("timed out"),
408+
OSError("dns failure"),
409+
],
410+
ids=["client_error", "timeout", "os_error"],
411+
)
412+
async def test_prepared_request_wraps_network_errors(exception: Exception, mock_rest: Any) -> None:
413+
"""Network errors from aiohttp must be wrapped as RoborockException so
414+
consumers can rely on the typed exception hierarchy."""
415+
url_pattern = re.compile(r"https://example\.com/api/test.*")
416+
mock_rest.post(url_pattern, exception=exception)
417+
418+
req = PreparedRequest("https://example.com")
419+
with pytest.raises(RoborockException) as exc_info:
420+
await req.request("post", "/api/test")
421+
assert exc_info.value.__cause__ is exception

0 commit comments

Comments
 (0)