Skip to content
This repository was archived by the owner on Jul 10, 2026. It is now read-only.

Commit d361f28

Browse files
committed
fix(networking): fix ip-lookup fallback chain
Closes #3309. Each provider in `get_external_ip` was wrapped in `except ExternalIPNotFound`, but none of the underlying calls (`requests.get`, `ip_to_int`, `os.popen`+`readline`, `urllib.request.urlopen`, `json.loads`, wikipedia header lookup) ever raise `ExternalIPNotFound`. As a result the first failing provider crashed the entire function instead of falling through to the next one. Catch the exceptions that can actually occur: * RequestException — for `requests.get` failures * AddrFormatError — for malformed IPs * OSError — for `os.popen`/curl failures * URLError — for urllib failures * ValueError — for json + int parse failures * KeyError — for the wikipedia header * AssertionError — for the isinstance guards * ExternalIPNotFound — kept for backward compat Three regression tests cover: AWS connection error → curl fallthrough, malformed AWS IP → curl fallthrough, and all-providers-exhausted → ExternalIPNotFound.
1 parent ad2a8e4 commit d361f28

2 files changed

Lines changed: 96 additions & 6 deletions

File tree

bittensor/utils/networking.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import os
44
from typing import Optional
5+
from urllib import error as urllib_error
56
from urllib import request as urllib_request
67

78
import netaddr
@@ -13,6 +14,22 @@ class ExternalIPNotFound(Exception):
1314
"""Raised if we cannot attain your external ip from CURL/URLLIB/IPIFY/AWS"""
1415

1516

17+
# Exceptions each provider in `get_external_ip` can raise on failure. Caught per
18+
# provider so that a single failing lookup falls through to the next one instead
19+
# of crashing the whole function. Kept as a tuple so the list stays consistent
20+
# across providers.
21+
_IP_LOOKUP_EXCEPTIONS = (
22+
requests.exceptions.RequestException,
23+
netaddr.core.AddrFormatError,
24+
OSError,
25+
urllib_error.URLError,
26+
ValueError,
27+
KeyError,
28+
AssertionError,
29+
ExternalIPNotFound,
30+
)
31+
32+
1633
def int_to_ip(int_val: int) -> str:
1734
"""Maps an integer to a unique ip-string
1835
@@ -68,7 +85,7 @@ def get_external_ip() -> str:
6885
external_ip = requests.get("https://checkip.amazonaws.com").text.strip()
6986
assert isinstance(ip_to_int(external_ip), int)
7087
return str(external_ip)
71-
except ExternalIPNotFound:
88+
except _IP_LOOKUP_EXCEPTIONS:
7289
pass
7390

7491
# --- Try ipconfig.
@@ -78,7 +95,7 @@ def get_external_ip() -> str:
7895
process.close()
7996
assert isinstance(ip_to_int(external_ip), int)
8097
return str(external_ip)
81-
except ExternalIPNotFound:
98+
except _IP_LOOKUP_EXCEPTIONS:
8299
pass
83100

84101
# --- Try ipinfo.
@@ -88,7 +105,7 @@ def get_external_ip() -> str:
88105
process.close()
89106
assert isinstance(ip_to_int(external_ip), int)
90107
return str(external_ip)
91-
except ExternalIPNotFound:
108+
except _IP_LOOKUP_EXCEPTIONS:
92109
pass
93110

94111
# --- Try myip.dnsomatic
@@ -98,23 +115,23 @@ def get_external_ip() -> str:
98115
process.close()
99116
assert isinstance(ip_to_int(external_ip), int)
100117
return str(external_ip)
101-
except ExternalIPNotFound:
118+
except _IP_LOOKUP_EXCEPTIONS:
102119
pass
103120

104121
# --- Try urllib ipv6
105122
try:
106123
external_ip = urllib_request.urlopen("https://ident.me").read().decode("utf8")
107124
assert isinstance(ip_to_int(external_ip), int)
108125
return str(external_ip)
109-
except ExternalIPNotFound:
126+
except _IP_LOOKUP_EXCEPTIONS:
110127
pass
111128

112129
# --- Try Wikipedia
113130
try:
114131
external_ip = requests.get("https://www.wikipedia.org").headers["X-Client-IP"]
115132
assert isinstance(ip_to_int(external_ip), int)
116133
return str(external_ip)
117-
except ExternalIPNotFound:
134+
except _IP_LOOKUP_EXCEPTIONS:
118135
pass
119136

120137
raise ExternalIPNotFound

tests/unit_tests/utils/test_networking.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,79 @@ def urlopen(self):
161161
assert utils.networking.get_external_ip()
162162

163163

164+
# Regression for https://github.com/latent-to/bittensor/issues/3309:
165+
# providers that raise their real-world exceptions (not `ExternalIPNotFound`)
166+
# must be caught and allow the fallback chain to continue.
167+
def test_get_external_ip_aws_connection_error_falls_through_to_curl(mocker):
168+
"""AWS requests.ConnectionError should fall through to the curl provider."""
169+
mocked_requests_get = mock.Mock(
170+
side_effect=requests.exceptions.ConnectionError("no network"),
171+
)
172+
mocker.patch.object(requests, "get", mocked_requests_get)
173+
174+
class FakeProcess:
175+
def readline(self):
176+
return "203.0.113.7"
177+
178+
def close(self):
179+
return None
180+
181+
def read(self):
182+
return '{"ip": "203.0.113.7"}'
183+
184+
mocker.patch.object(os, "popen", mock.Mock(return_value=FakeProcess()))
185+
186+
assert utils.networking.get_external_ip() == "203.0.113.7"
187+
188+
189+
def test_get_external_ip_malformed_ip_falls_through(mocker):
190+
"""Malformed IP from AWS should raise AddrFormatError, not propagate."""
191+
mocked_requests_get = mock.Mock(
192+
return_value=mock.Mock(
193+
**{"text": "not-an-ip"},
194+
),
195+
)
196+
mocker.patch.object(requests, "get", mocked_requests_get)
197+
198+
class FakeProcess:
199+
def readline(self):
200+
return "198.51.100.42"
201+
202+
def close(self):
203+
return None
204+
205+
def read(self):
206+
return '{"ip": "198.51.100.42"}'
207+
208+
mocker.patch.object(os, "popen", mock.Mock(return_value=FakeProcess()))
209+
210+
assert utils.networking.get_external_ip() == "198.51.100.42"
211+
212+
213+
def test_get_external_ip_all_providers_exhausted_raises(mocker):
214+
"""If every provider fails, the function must raise ExternalIPNotFound
215+
(not the last underlying exception)."""
216+
mocker.patch.object(
217+
requests,
218+
"get",
219+
mock.Mock(side_effect=requests.exceptions.ConnectionError("no network")),
220+
)
221+
mocker.patch.object(
222+
os,
223+
"popen",
224+
mock.Mock(side_effect=OSError("popen disabled")),
225+
)
226+
# Patch through the module's own imported name to avoid interference from
227+
# other tests in this file that rebind `urllib.request` at module scope.
228+
mocker.patch(
229+
"bittensor.utils.networking.urllib_request.urlopen",
230+
mock.Mock(side_effect=urllib.error.URLError("urlopen disabled")),
231+
)
232+
233+
with pytest.raises(utils.networking.ExternalIPNotFound):
234+
utils.networking.get_external_ip()
235+
236+
164237
# Test formatting WebSocket endpoint URL
165238
@pytest.mark.parametrize(
166239
"url, expected",

0 commit comments

Comments
 (0)