diff --git a/esphome/arduino8266/framework.py b/esphome/arduino8266/framework.py index cac7bd9905..843e3a2022 100644 --- a/esphome/arduino8266/framework.py +++ b/esphome/arduino8266/framework.py @@ -138,9 +138,20 @@ def _registry_download(package: str, version: str) -> tuple[str, str, int | None import requests url = _REGISTRY_URL.format(package=package) - resp = requests.get(url, timeout=30) - resp.raise_for_status() - data = resp.json() + last_err: Exception | None = None + for _ in range(3): + try: + resp = requests.get(url, timeout=30) + resp.raise_for_status() + data = resp.json() + break + except requests.RequestException as err: + last_err = err + else: + # A clean, retried error like the other download paths in the tree + raise EsphomeError( + f"Could not query the package registry for {package}: {last_err}" + ) from last_err system = _pio_system() for ver in data.get("versions", []): if ver.get("name") != version: diff --git a/tests/unit_tests/test_arduino8266_framework.py b/tests/unit_tests/test_arduino8266_framework.py index f786d22ba8..d17250a49b 100644 --- a/tests/unit_tests/test_arduino8266_framework.py +++ b/tests/unit_tests/test_arduino8266_framework.py @@ -81,6 +81,42 @@ def _registry_response(files: list[dict]) -> MagicMock: return resp +def test_registry_download_network_error_is_clean_and_retried() -> None: + """Registry failures raise EsphomeError after retries, not a traceback.""" + import requests + + with ( + patch("requests.get", side_effect=requests.ConnectionError("boom")) as mock_get, + pytest.raises(EsphomeError, match="Could not query the package registry"), + ): + framework._registry_download("pkg", "1.0.0") + assert mock_get.call_count == 3 + + +def test_registry_download_retries_transient_error() -> None: + import requests + + resp = _registry_response( + [ + { + "system": ["linux_x86_64"], + "download_url": "http://x/linux", + "checksum": {"sha256": "abc123"}, + "size": 42, + } + ] + ) + with ( + patch("requests.get", side_effect=[requests.ConnectionError("boom"), resp]), + patch.object(framework, "_pio_system", return_value="linux_x86_64"), + ): + assert framework._registry_download("pkg", "1.0.0") == ( + "http://x/linux", + "abc123", + 42, + ) + + def test_registry_download_matches_system() -> None: resp = _registry_response( [