Skip to content

Commit 5184d0c

Browse files
committed
Validate HTTP Range responses in subset downloads to prevent silent disk blowup
1 parent 23040b2 commit 5184d0c

2 files changed

Lines changed: 64 additions & 0 deletions

File tree

src/herbie/core.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1098,8 +1098,29 @@ def subset(search, outFile, verbose=True):
10981098
grib_source,
10991099
headers=headers,
11001100
timeout=30,
1101+
stream=True,
11011102
)
11021103
response.raise_for_status()
1104+
1105+
# Guard: if the server did not honor the Range
1106+
# request, each group would silently download the
1107+
# entire file and append it, ballooning disk usage.
1108+
if response.status_code != 206:
1109+
content_length = response.headers.get(
1110+
"Content-Length", "unknown"
1111+
)
1112+
response.close()
1113+
raise RuntimeError(
1114+
f"Range request not honored: server returned "
1115+
f"HTTP {response.status_code} "
1116+
f"(Content-Length: {content_length}). "
1117+
f"This can happen when a network proxy or "
1118+
f"VPN strips the Range header. Try downloading "
1119+
f"the full file first, then subset locally:\n"
1120+
f" full_file = Herbie(...).download()\n"
1121+
f" Herbie(...).download(search=...)"
1122+
)
1123+
11031124
data = response.content
11041125

11051126
# Write or append to output file
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""Test that subset downloads validate HTTP Range request responses."""
2+
3+
from datetime import datetime, timedelta
4+
from unittest.mock import Mock, patch
5+
6+
import pytest
7+
8+
from herbie import Herbie, config
9+
10+
now = datetime.now()
11+
today = datetime(now.year, now.month, now.day, now.hour) - timedelta(hours=6)
12+
13+
save_dir = config["default"]["save_dir"] / "Herbie-Tests-Data/"
14+
15+
16+
def test_subset_raises_on_non_206_response():
17+
"""RuntimeError should fire when the server returns 200 instead of 206.
18+
19+
This prevents the silent disk-space blowup described in issue #514:
20+
without the guard, each subset group downloads the entire multi-GB
21+
file and appends it, producing output many times larger than the
22+
source.
23+
"""
24+
H = Herbie(
25+
today,
26+
model="hrrr",
27+
product="sfc",
28+
save_dir=save_dir,
29+
overwrite=True,
30+
)
31+
32+
# Force the index to be fetched and cached before we patch requests.
33+
_ = H.index_as_dataframe
34+
35+
mock_response = Mock()
36+
mock_response.status_code = 200
37+
mock_response.headers = {"Content-Length": "9999999999"}
38+
mock_response.raise_for_status = Mock()
39+
mock_response.close = Mock()
40+
41+
with patch("herbie.core.requests.get", return_value=mock_response):
42+
with pytest.raises(RuntimeError, match="Range request not honored"):
43+
H.download("TMP:2 m", overwrite=True)

0 commit comments

Comments
 (0)