Repository: encode/uvicorn

Issue:
reload option works only with IPv4 host
I can not specify --reload option with IPv6 only --host.
`uvicorn --reload --host="::" example:app`

The server binds only to IPv4 host.



Discussion:
Im noticing something similar if I set `--workers 4` on my rpi3. 
Was about to report the same thing, but with unix sockets.
    if config.should_reload:
        sock = config.bind_socket()
        supervisor = ChangeReload(config, target=server.run, sockets=[sock])
        supervisor.run()
    elif config.workers > 1:
        sock = config.bind_socket()
        supervisor = Multiprocess(config, target=server.run, sockets=[sock])
        supervisor.run()
    else:
        server.run()

seems to be the offending part.

config.bind_socket() has no code to deal with unix sockets, that's in the Server class.



Resolving patch:
diff --git a/uvicorn/config.py b/uvicorn/config.py
--- a/uvicorn/config.py
+++ b/uvicorn/config.py
@@ -115,20 +115,6 @@ def create_ssl_context(
     return ctx
 
 
-def _get_server_start_message(is_ipv6_message: bool = False) -> Tuple[str, str]:
-    if is_ipv6_message:
-        ip_repr = "%s://[%s]:%d"
-    else:
-        ip_repr = "%s://%s:%d"
-    message = f"Uvicorn running on {ip_repr} (Press CTRL+C to quit)"
-    color_message = (
-        "Uvicorn running on "
-        + click.style(ip_repr, bold=True)
-        + " (Press CTRL+C to quit)"
-    )
-    return message, color_message
-
-
 class Config:
     def __init__(
         self,
@@ -355,10 +341,15 @@ def setup_event_loop(self):
             loop_setup()
 
     def bind_socket(self):
-        family, sockettype, proto, canonname, sockaddr = socket.getaddrinfo(
-            self.host, self.port, type=socket.SOCK_STREAM
-        )[0]
-        sock = socket.socket(family=family, type=sockettype)
+        family = socket.AF_INET
+        addr_format = "%s://%s:%d"
+
+        if self.host and ":" in self.host:
+            # It's an IPv6 address.
+            family = socket.AF_INET6
+            addr_format = "%s://[%s]:%d"
+
+        sock = socket.socket(family=family)
         sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
         try:
             sock.bind((self.host, self.port))
@@ -367,10 +358,12 @@ def bind_socket(self):
             sys.exit(1)
         sock.set_inheritable(True)
 
-        if family == socket.AddressFamily.AF_INET6:
-            message, color_message = _get_server_start_message(is_ipv6_message=True)
-        else:
-            message, color_message = _get_server_start_message()
+        message = f"Uvicorn running on {addr_format} (Press CTRL+C to quit)"
+        color_message = (
+            "Uvicorn running on "
+            + click.style(addr_format, bold=True)
+            + " (Press CTRL+C to quit)"
+        )
         protocol_name = "https" if self.is_ssl else "http"
         logger.info(
             message,
diff --git a/uvicorn/main.py b/uvicorn/main.py
--- a/uvicorn/main.py
+++ b/uvicorn/main.py
@@ -10,7 +10,6 @@
 import time
 import typing
 from email.utils import formatdate
-from ipaddress import IPv4Address, IPv6Address, ip_address
 
 import click
 
@@ -25,7 +24,6 @@
     SSL_PROTOCOL_VERSION,
     WS_PROTOCOLS,
     Config,
-    _get_server_start_message,
 )
 from uvicorn.supervisors import ChangeReload, Multiprocess
 
@@ -506,6 +504,11 @@ def _share_socket(sock: socket) -> socket:
 
         else:
             # Standard case. Create a socket from a host/port pair.
+            addr_format = "%s://%s:%d"
+            if config.host and ":" in config.host:
+                # It's an IPv6 address.
+                addr_format = "%s://[%s]:%d"
+
             try:
                 server = await loop.create_server(
                     create_protocol,
@@ -522,17 +525,12 @@ def _share_socket(sock: socket) -> socket:
             if port == 0:
                 port = server.sockets[0].getsockname()[1]
             protocol_name = "https" if config.ssl else "http"
-            try:
-                addr = ip_address(config.host)
-                if isinstance(addr, IPv6Address):
-                    message, color_message = _get_server_start_message(
-                        is_ipv6_message=True
-                    )
-                elif isinstance(addr, IPv4Address):
-                    message, color_message = _get_server_start_message()
-            except ValueError:
-                message, color_message = _get_server_start_message()
-
+            message = f"Uvicorn running on {addr_format} (Press CTRL+C to quit)"
+            color_message = (
+                "Uvicorn running on "
+                + click.style(addr_format, bold=True)
+                + " (Press CTRL+C to quit)"
+            )
             logger.info(
                 message,
                 protocol_name,


Test patch:
diff --git a/tests/test_main.py b/tests/test_main.py
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -2,13 +2,22 @@
 import threading
 import time
 
+import pytest
 import requests
 
 from uvicorn.config import Config
 from uvicorn.main import Server
 
 
-def test_run():
+@pytest.mark.parametrize(
+    "host, url",
+    [
+        pytest.param(None, "http://127.0.0.1:8000", id="default"),
+        pytest.param("localhost", "http://127.0.0.1:8000", id="hostname"),
+        pytest.param("::1", "http://[::1]:8000", id="ipv6"),
+    ],
+)
+def test_run(host, url):
     class App:
         def __init__(self, scope):
             if scope["type"] != "http":
@@ -22,38 +31,13 @@ class CustomServer(Server):
         def install_signal_handlers(self):
             pass
 
-    config = Config(app=App, loop="asyncio", limit_max_requests=1)
+    config = Config(app=App, host=host, loop="asyncio", limit_max_requests=1)
     server = CustomServer(config=config)
     thread = threading.Thread(target=server.run)
     thread.start()
     while not server.started:
         time.sleep(0.01)
-    response = requests.get("http://127.0.0.1:8000")
-    assert response.status_code == 204
-    thread.join()
-
-
-def test_run_hostname():
-    class App:
-        def __init__(self, scope):
-            if scope["type"] != "http":
-                raise Exception()
-
-        async def __call__(self, receive, send):
-            await send({"type": "http.response.start", "status": 204, "headers": []})
-            await send({"type": "http.response.body", "body": b"", "more_body": False})
-
-    class CustomServer(Server):
-        def install_signal_handlers(self):
-            pass
-
-    config = Config(app=App, host="localhost", loop="asyncio", limit_max_requests=1)
-    server = CustomServer(config=config)
-    thread = threading.Thread(target=server.run)
-    thread.start()
-    while not server.started:
-        time.sleep(0.01)
-    response = requests.get("http://localhost:8000")
+    response = requests.get(url)
     assert response.status_code == 204
     thread.join()