Skip to content

Conversation

cgoldberg
Copy link
Member

@cgoldberg cgoldberg commented Jul 6, 2025

User description

🔗 Related Issues

Fixes #14910 for Python

💥 What does this PR do?

This PR updates the free_port() function in selenium.webdriver.common.utils so it will bind to localhost using IPv6 (::1) if IPv4 (127.0.0.1) is not available.

Without this fix, Selenium can't be used on an IPv6-only system without some additional code.

For example, if you are on IPv6-only system, the following code will fail:

from selenium import webdriver driver = webdriver.Chrome() 

with OSError: [Errno 99] Cannot assign requested address

With the changes in the PR, it will work.

Note: geckodriver doesn't work on IPv6-only systems, so Firefox still won't work.


Notes on testing:

I didn't have an IPv6-only system to test on, so I used a a mixed environment and disabled IPv4 interfaces. Here are instructions for setting that up on Debian/Ubuntu Linux:

To see interface names and IP addresses, run ip addr show.

For example, I get the following output:

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 5: eth0@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000 link/ether 00:16:3e:70:a6:12 brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet 100.115.92.196/28 brd 100.115.92.207 scope global eth0 valid_lft forever preferred_lft forever inet6 2601:189:8181:33b0:216:3eff:fe70:a612/64 scope global dynamic mngtmpaddr valid_lft 345599sec preferred_lft 345599sec inet6 fe80::216:3eff:fe70:a612/64 scope link valid_lft forever preferred_lft forever 

You can see I have the following network interfaces with IPv4 addresses:

lo: 127.0.0.1/8 eth0: 100.115.92.196/28 

I can disable these by running:

sudo ip addr del 100.115.92.196/28 dev eth0 sudo ip addr del 127.0.0.1/8 dev lo 

Now I am running on a system that only allows IPv6 and can test.

(these changes will be reverted on reboot)

🔄 Types of changes

  • Bug fix (backwards compatible)

PR Type

Enhancement


Description

  • Add IPv6 fallback support to free_port() function

  • Enable Selenium to work on IPv6-only systems

  • Maintain backward compatibility with IPv4 systems


Changes diagram

flowchart LR A["free_port() called"] --> B["Try IPv4 bind"] B --> C{IPv4 available?} C -->|Yes| D["Bind to 127.0.0.1"] C -->|No| E["Fallback to IPv6"] E --> F["Bind to ::1"] D --> G["Return port"] F --> G 
Loading

Changes walkthrough 📝

Relevant files
Enhancement
utils.py
Add IPv6 fallback to free_port function                                   

py/selenium/webdriver/common/utils.py

  • Modified free_port() to try IPv4 first, fallback to IPv6
  • Added exception handling for OSError when IPv4 binding fails
  • Updated function docstring to explain IPv6 fallback behavior
  • +12/-3   

    Need help?
  • Type /help how to ... in the comments thread for any questions about Qodo Merge usage.
  • Check out the documentation for more information.
  • @selenium-ci selenium-ci added the C-py Python Bindings label Jul 6, 2025
    Copy link
    Contributor

    qodo-merge-pro bot commented Jul 6, 2025

    PR Reviewer Guide 🔍

    Here are some key observations to aid the review process:

    ⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
    🧪 No relevant tests
    🔒 No security concerns identified
    Copy link
    Contributor

    qodo-merge-pro bot commented Jul 6, 2025

    PR Code Suggestions ✨

    Latest suggestions up to e1b3716

    CategorySuggestion                                                                                                                                    Impact
    Incremental [*]
    Handle IPv6 binding failures gracefully
    Suggestion Impact:The suggestion was implemented by wrapping the IPv6 socket creation and binding in a try-except block, with a RuntimeError raised when both IPv4 and IPv6 fail

    code diff:

    + try: + free_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + free_socket.bind(("::1", 0)) + except OSError: + raise RuntimeError("Can't find free port (Unable to bind to IPv4 or IPv6)")

    Add a try-except block around the IPv6 socket creation and binding to handle
    cases where IPv6 is also unavailable, preventing potential unhandled exceptions
    that could crash the function.

    py/selenium/webdriver/common/utils.py [34-44]

     free_socket = None try: # IPv4 free_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) free_socket.bind(("127.0.0.1", 0)) except OSError: if free_socket: free_socket.close() - # IPv6 - free_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) - free_socket.bind(("::1", 0)) + try: + # IPv6 + free_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + free_socket.bind(("::1", 0)) + except OSError: + raise RuntimeError("Unable to bind to both IPv4 and IPv6")

    [Suggestion processed]

    Suggestion importance[1-10]: 7

    __

    Why: The suggestion correctly identifies that if both IPv4 and IPv6 binding fail, an unhandled OSError would occur, and proposes a robust solution to catch this.

    Medium
    Learned
    best practice
    Ensure proper socket cleanup
    Suggestion Impact:The suggestion was implemented with the try-finally block to ensure socket cleanup, but the commit also added additional error handling and exception wrapping that wasn't in the original suggestion

    code diff:

    + try: + free_socket.listen(5) + port: int = free_socket.getsockname()[1] + except Exception as e: + raise RuntimeError(f"Can't find free port ({e})") + finally: + free_socket.close()

    The socket resource is not properly cleaned up if an exception occurs after the
    IPv6 socket creation or during listen/getsockname operations. Use a try-finally
    block to ensure the socket is always closed, preventing resource leaks.

    py/selenium/webdriver/common/utils.py [45-47]

     free_socket = None try: # IPv4 free_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) free_socket.bind(("127.0.0.1", 0)) except OSError: if free_socket: free_socket.close() # IPv6 free_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) free_socket.bind(("::1", 0)) -free_socket.listen(5) -port: int = free_socket.getsockname()[1] -free_socket.close() +try: + free_socket.listen(5) + port: int = free_socket.getsockname()[1] +finally: + free_socket.close() +

    [To ensure code accuracy, apply this suggestion manually]

    Suggestion importance[1-10]: 6

    __

    Why:
    Relevant best practice - Ensure proper resource cleanup by closing sockets, processes, and drivers in finally blocks or using context managers, and check process state before attempting to terminate to prevent resource leaks and exceptions.

    Low
    • Update

    Previous suggestions

    ✅ Suggestions up to commit ee82768
    CategorySuggestion                                                                                                                                    Impact
    Possible issue
    Prevent socket resource leak
    Suggestion Impact:The suggestion was directly implemented - the commit adds the exact code changes suggested to prevent socket resource leaks by initializing free_socket to None and closing it if binding fails

    code diff:

    + free_socket = None try: # IPv4 free_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) free_socket.bind(("127.0.0.1", 0)) except OSError: + if free_socket: + free_socket.close()

    The IPv4 socket should be closed if binding fails to prevent resource leaks.
    Currently, if IPv4 socket creation succeeds but binding fails, the socket
    remains open when the exception is caught.

    py/selenium/webdriver/common/utils.py [34-44]

    +free_socket = None try: # IPv4 free_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) free_socket.bind(("127.0.0.1", 0)) except OSError: + if free_socket: + free_socket.close() # IPv6 free_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) free_socket.bind(("::1", 0)) free_socket.listen(5) port: int = free_socket.getsockname()[1] free_socket.close()

    [Suggestion processed]

    Suggestion importance[1-10]: 8

    __

    Why: The suggestion correctly identifies a resource leak where the IPv4 socket is not closed if bind() fails, which is a valid correctness issue introduced in the PR.

    Medium
    @nvborisenko
    Copy link
    Member

    @cgoldberg do you know how to easily test it?

    @cgoldberg
    Copy link
    Member Author

    @nvborisenko read my PR ... I wrote a whole section on how to test it :)

    (for Linux at least)

    @nvborisenko
    Copy link
    Member

    I tried your steps to disable IPv4 interfaces on Ubuntu VM, and even cannot reproduce the issue when finding available port should fail :(

    @cgoldberg cgoldberg merged commit bc88096 into SeleniumHQ:trunk Jul 14, 2025
    16 checks passed
    @cgoldberg cgoldberg deleted the py-support-ipv6-only-systems branch July 14, 2025 02:55
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Labels

    4 participants