Skip to content

Commit 2465da4

Browse files
committed
Handle non-string redirect URLs
1 parent b1850da commit 2465da4

2 files changed

Lines changed: 20 additions & 1 deletion

File tree

guidance/_utils.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@ def bytes_from(
5050
with urllib.request.urlopen(src, timeout=timeout) as response:
5151
response = cast(http.client.HTTPResponse, response)
5252
final_url = response.geturl()
53-
if isinstance(final_url, str) and final_url != src:
53+
if not isinstance(final_url, str):
54+
raise TypeError(
55+
f"Expected urlopen response URL to be a string, got {type(final_url).__name__}"
56+
)
57+
if final_url != src:
5458
validate_uri(
5559
final_url,
5660
allowed_schemes=allowed_schemes,

tests/unit/test_bytes_from.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ def test_allows_http_when_configured(self):
1717
with patch("urllib.request.urlopen") as mock_urlopen:
1818
mock_urlopen.return_value.__enter__ = lambda s: s
1919
mock_urlopen.return_value.__exit__ = lambda s, *a: None
20+
mock_urlopen.return_value.geturl.return_value = "http://example.com/file.bin"
2021
mock_urlopen.return_value.read.return_value = b"data"
2122
with patch("guidance._uri_validation._validate_hostname_not_private"):
2223
bytes_from("http://example.com/file.bin", allow_local=True, allowed_schemes=("http", "https"))
@@ -31,6 +32,7 @@ def test_allows_private_when_configured(self):
3132
with patch("urllib.request.urlopen") as mock_urlopen:
3233
mock_urlopen.return_value.__enter__ = lambda s: s
3334
mock_urlopen.return_value.__exit__ = lambda s, *a: None
35+
mock_urlopen.return_value.geturl.return_value = "https://internal.example.com/file.bin"
3436
mock_urlopen.return_value.read.return_value = b"data"
3537
result = bytes_from("https://internal.example.com/file.bin", allow_local=True, allow_private=True)
3638
assert result == b"data"
@@ -70,6 +72,18 @@ def fake_getaddrinfo(hostname, *args, **kwargs):
7072

7173
response.read.assert_not_called()
7274

75+
def test_rejects_non_string_redirect_target(self):
76+
with patch("socket.getaddrinfo", return_value=[(None, None, None, None, ("93.184.216.34", 443))]):
77+
with patch("urllib.request.urlopen") as mock_urlopen:
78+
response = mock_urlopen.return_value.__enter__.return_value
79+
response.geturl.return_value = b"https://example.com/file.bin"
80+
response.read.return_value = b"data"
81+
82+
with pytest.raises(TypeError, match="response URL to be a string"):
83+
bytes_from("https://example.com/file.bin", allow_local=True)
84+
85+
response.read.assert_not_called()
86+
7387
def test_local_path_still_works(self, tmp_path):
7488
test_file = tmp_path / "test.bin"
7589
test_file.write_bytes(b"local data")
@@ -109,6 +123,7 @@ def test_timeout_passed_to_urlopen(self):
109123
with patch("urllib.request.urlopen") as mock_urlopen:
110124
mock_urlopen.return_value.__enter__ = lambda s: s
111125
mock_urlopen.return_value.__exit__ = lambda s, *a: None
126+
mock_urlopen.return_value.geturl.return_value = "https://example.com/f"
112127
mock_urlopen.return_value.read.return_value = b"data"
113128
bytes_from("https://example.com/f", allow_local=True, timeout=42.0)
114129
mock_urlopen.assert_called_once_with("https://example.com/f", timeout=42.0)

0 commit comments

Comments
 (0)