Skip to content

fix: correct exception handling in get_external_ip fallback chain#3311

Open
Grizouforever wants to merge 2 commits intolatent-to:masterfrom
Grizouforever:fix/external-ip-exception-handling
Open

fix: correct exception handling in get_external_ip fallback chain#3311
Grizouforever wants to merge 2 commits intolatent-to:masterfrom
Grizouforever:fix/external-ip-exception-handling

Conversation

@Grizouforever
Copy link
Copy Markdown

Summary

Fixes #3309

The fallback chain in get_external_ip() catches ExternalIPNotFound, but none of the code inside the try blocks actually raises that exception. Real exceptions (requests.RequestException, AssertionError, OSError, json.JSONDecodeError, etc.) propagate unhandled, preventing fallthrough to the next IP provider. This means if the first provider (AWS checkip) fails, the function crashes instead of trying the remaining 5 providers.

Changes

bittensor/utils/networking.py:

  • Replace all except ExternalIPNotFound with except Exception to properly catch real exceptions
  • Replace deprecated os.popen() calls with subprocess.run() (Python 3.0+ recommended)
  • Replace bare assert isinstance(...) validation with direct ip_to_int() calls that raise on invalid input regardless of -O optimization flag

Context

PR #3262 previously attempted this fix but was closed due to being based on the wrong branch and author not responding to review. This PR is based on master (the current default branch) and incorporates the additional improvements suggested.

Test User and others added 2 commits April 11, 2026 13:32
The fallback chain in get_external_ip() catches ExternalIPNotFound
but none of the code inside the try blocks raises that exception.
The actual exceptions (requests.RequestException, AssertionError,
OSError, json.JSONDecodeError, etc.) propagate unhandled, preventing
fallthrough to the next IP provider.

Changes:
- Replace all `except ExternalIPNotFound` with `except Exception`
  to properly catch real exceptions and allow fallthrough
- Replace deprecated `os.popen()` calls with `subprocess.run()`
- Replace bare `assert` validation with direct `ip_to_int()` calls
  which raise on invalid input regardless of -O flag

Fixes latent-to#3309
The implementation in bittensor/utils/networking.py was updated in this
PR to replace os.popen("curl ...") with subprocess.run(...), but the
test test_get_external_ip_os_request_urllib_broken still mocked os.popen
which no longer intercepts any calls. Update the test to mock
subprocess.run and bittensor.utils.networking.urllib_request.urlopen so
all fallback paths are properly blocked and the expected ExternalIPNotFound
exception is raised.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@thewhaleking thewhaleking requested a review from a team April 14, 2026 10:19
@thewhaleking
Copy link
Copy Markdown
Contributor

Read CONTRIBUTING

Copy link
Copy Markdown
Collaborator

@basfroman basfroman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You really found a bug. I'll happily accept the PR, but please take a look at the comments and rebase your pr to staging branch.
Thanks for this contribution!

@@ -1,6 +1,7 @@
"""Utils for handling local network with ip and ports."""

import os
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dead import

"""Utils for handling local network with ip and ports."""

import os
import subprocess
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why use subprocess when requests is available?

Comment on lines +77 to +79
result = subprocess.run(
["curl", "-s", "ifconfig.me"], capture_output=True, text=True, timeout=10
)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

subprocess + curl -> requests.get

def mock_subprocess_run(*args, **kwargs):
raise OSError("subprocess.run mocked to fail")

class FakeResponse:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't FakeResponse dead now?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things missing on the test side:

First: test_get_external_ip_os_broken still patches os.popen, but this PR removes all os.popen usage from production code. That mock now does nothing; the test passes solely because requests.get is mocked to succeed on the first try. The test name and docstring say "when os.popen is broken," but it no longer tests that scenario.
Pls update it to mock subprocess.run failing (or whichever replacement we land on) while keeping requests.get succeeding, so it actually validates "first method works, broken fallback methods don't interfere."

Second: there's no test for the cascade itself. For example: mock requests.get to raise, mock the first subprocess.run call to return a valid IP -> assert the function returns that IP. This is the core contract of get_external_ip() - "try multiple sources in order" - and it has zero coverage today.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

get_external_ip fallback chain never triggers — all attempts catch ExternalIPNotFound but none of the called functions raise it

3 participants