mirror of
https://github.com/esphome/esphome.git
synced 2026-09-11 23:37:34 +00:00
[core] Catch body-read errors in download_content
`requests.Response.content` reads the body lazily, so chunked-decode, gzip-decode, and mid-stream connection drops all surface as RequestException subclasses on first access -- not from requests.get itself. The previous code accessed `.content` outside the surrounding try/except, so any of those (rare but real) errors would propagate out of download_content instead of falling back to the cached file or raising the user-friendly cv.Invalid. Move `data = req.content` inside the try block so the existing error path handles it. Two new tests cover the with-cache and no-cache branches using a `ChunkedEncodingError` injected on `.content`.
This commit is contained in:
@@ -238,6 +238,59 @@ def test_download_content_with_network_error_no_cache_fails(
|
||||
external_files.download_content(url, test_file)
|
||||
|
||||
|
||||
@patch("esphome.external_files.requests.get")
|
||||
@patch("esphome.external_files.has_remote_file_changed")
|
||||
def test_download_content_with_body_read_error_uses_cache(
|
||||
mock_has_changed: MagicMock, mock_get: MagicMock, setup_core: Path
|
||||
) -> None:
|
||||
"""Body-read errors (chunked-decode/gzip-decode/mid-stream connection
|
||||
drop) raise RequestException subclasses on `.content` access, not from
|
||||
`requests.get` itself. They must follow the same fall-back-to-cache
|
||||
path as a connect-time failure.
|
||||
"""
|
||||
test_file = setup_core / "cached.txt"
|
||||
cached_content = b"cached content"
|
||||
test_file.write_bytes(cached_content)
|
||||
|
||||
mock_has_changed.return_value = True
|
||||
mock_response = MagicMock()
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
type(mock_response).content = property(
|
||||
lambda self: (_ for _ in ()).throw(
|
||||
requests.exceptions.ChunkedEncodingError("body truncated")
|
||||
)
|
||||
)
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
result = external_files.download_content("https://example.com/file.txt", test_file)
|
||||
|
||||
assert result == cached_content
|
||||
|
||||
|
||||
@patch("esphome.external_files.requests.get")
|
||||
@patch("esphome.external_files.has_remote_file_changed")
|
||||
def test_download_content_with_body_read_error_no_cache_fails(
|
||||
mock_has_changed: MagicMock, mock_get: MagicMock, setup_core: Path
|
||||
) -> None:
|
||||
"""A body-read failure with no cache available must surface as a
|
||||
cv.Invalid, same as a connect-time failure with no cache.
|
||||
"""
|
||||
test_file = setup_core / "nonexistent.txt"
|
||||
|
||||
mock_has_changed.return_value = True
|
||||
mock_response = MagicMock()
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
type(mock_response).content = property(
|
||||
lambda self: (_ for _ in ()).throw(
|
||||
requests.exceptions.ChunkedEncodingError("body truncated")
|
||||
)
|
||||
)
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
with pytest.raises(Invalid, match="Could not download from.*body truncated"):
|
||||
external_files.download_content("https://example.com/file.txt", test_file)
|
||||
|
||||
|
||||
@patch("esphome.external_files.requests.get")
|
||||
@patch("esphome.external_files.has_remote_file_changed")
|
||||
def test_download_content_skip_external_update_uses_cache(
|
||||
|
||||
Reference in New Issue
Block a user