Skip to content

Commit bdab488

Browse files
Rework IPv6 support (#837)
* Revert "Fix server not running with explicit hostname (#827)" This reverts commit d78394e. * Revert "Fix reload with ipv6 host (#803)" This reverts commit 5acaee5. * Rework IPv6 support * Fix IPv6 localhost equivalent Co-authored-by: euri10 <[email protected]> * Reduce diff size * More diff size reduction * Fix: self.host -> config.host Co-authored-by: euri10 <[email protected]>
1 parent 6468b70 commit bdab488

File tree

4 files changed

+39
-64
lines changed

4 files changed

+39
-64
lines changed

docs/settings.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ equivalent keyword arguments, eg. `uvicorn.run("example:app", port=5000, reload=
1111

1212
## Socket Binding
1313

14-
* `--host <str>` - Bind socket to this host. Use `--host 0.0.0.0` to make the application available on your local network. **Default:** *'127.0.0.1'*.
14+
* `--host <str>` - Bind socket to this host. Use `--host 0.0.0.0` to make the application available on your local network. IPv6 addresses are supported, for example: `--host '::'`. **Default:** *'127.0.0.1'*.
1515
* `--port <int>` - Bind to a socket with this port. **Default:** *8000*.
1616
* `--uds <str>` - Bind to a UNIX domain socket. Useful if you want to run Uvicorn behind a reverse proxy.
1717
* `--fd <int>` - Bind to socket from this file descriptor. Useful if you want to run Uvicorn within a process manager.

tests/test_main.py

+12-28
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,22 @@
22
import threading
33
import time
44

5+
import pytest
56
import requests
67

78
from uvicorn.config import Config
89
from uvicorn.main import Server
910

1011

11-
def test_run():
12+
@pytest.mark.parametrize(
13+
"host, url",
14+
[
15+
pytest.param(None, "http://127.0.0.1:8000", id="default"),
16+
pytest.param("localhost", "http://127.0.0.1:8000", id="hostname"),
17+
pytest.param("::1", "http://[::1]:8000", id="ipv6"),
18+
],
19+
)
20+
def test_run(host, url):
1221
class App:
1322
def __init__(self, scope):
1423
if scope["type"] != "http":
@@ -22,38 +31,13 @@ class CustomServer(Server):
2231
def install_signal_handlers(self):
2332
pass
2433

25-
config = Config(app=App, loop="asyncio", limit_max_requests=1)
34+
config = Config(app=App, host=host, loop="asyncio", limit_max_requests=1)
2635
server = CustomServer(config=config)
2736
thread = threading.Thread(target=server.run)
2837
thread.start()
2938
while not server.started:
3039
time.sleep(0.01)
31-
response = requests.get("http://127.0.0.1:8000")
32-
assert response.status_code == 204
33-
thread.join()
34-
35-
36-
def test_run_hostname():
37-
class App:
38-
def __init__(self, scope):
39-
if scope["type"] != "http":
40-
raise Exception()
41-
42-
async def __call__(self, receive, send):
43-
await send({"type": "http.response.start", "status": 204, "headers": []})
44-
await send({"type": "http.response.body", "body": b"", "more_body": False})
45-
46-
class CustomServer(Server):
47-
def install_signal_handlers(self):
48-
pass
49-
50-
config = Config(app=App, host="localhost", loop="asyncio", limit_max_requests=1)
51-
server = CustomServer(config=config)
52-
thread = threading.Thread(target=server.run)
53-
thread.start()
54-
while not server.started:
55-
time.sleep(0.01)
56-
response = requests.get("http://localhost:8000")
40+
response = requests.get(url)
5741
assert response.status_code == 204
5842
thread.join()
5943

uvicorn/config.py

+15-22
Original file line numberDiff line numberDiff line change
@@ -115,20 +115,6 @@ def create_ssl_context(
115115
return ctx
116116

117117

118-
def _get_server_start_message(is_ipv6_message: bool = False) -> Tuple[str, str]:
119-
if is_ipv6_message:
120-
ip_repr = "%s://[%s]:%d"
121-
else:
122-
ip_repr = "%s://%s:%d"
123-
message = f"Uvicorn running on {ip_repr} (Press CTRL+C to quit)"
124-
color_message = (
125-
"Uvicorn running on "
126-
+ click.style(ip_repr, bold=True)
127-
+ " (Press CTRL+C to quit)"
128-
)
129-
return message, color_message
130-
131-
132118
class Config:
133119
def __init__(
134120
self,
@@ -355,10 +341,15 @@ def setup_event_loop(self):
355341
loop_setup()
356342

357343
def bind_socket(self):
358-
family, sockettype, proto, canonname, sockaddr = socket.getaddrinfo(
359-
self.host, self.port, type=socket.SOCK_STREAM
360-
)[0]
361-
sock = socket.socket(family=family, type=sockettype)
344+
family = socket.AF_INET
345+
addr_format = "%s://%s:%d"
346+
347+
if self.host and ":" in self.host:
348+
# It's an IPv6 address.
349+
family = socket.AF_INET6
350+
addr_format = "%s://[%s]:%d"
351+
352+
sock = socket.socket(family=family)
362353
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
363354
try:
364355
sock.bind((self.host, self.port))
@@ -367,10 +358,12 @@ def bind_socket(self):
367358
sys.exit(1)
368359
sock.set_inheritable(True)
369360

370-
if family == socket.AddressFamily.AF_INET6:
371-
message, color_message = _get_server_start_message(is_ipv6_message=True)
372-
else:
373-
message, color_message = _get_server_start_message()
361+
message = f"Uvicorn running on {addr_format} (Press CTRL+C to quit)"
362+
color_message = (
363+
"Uvicorn running on "
364+
+ click.style(addr_format, bold=True)
365+
+ " (Press CTRL+C to quit)"
366+
)
374367
protocol_name = "https" if self.is_ssl else "http"
375368
logger.info(
376369
message,

uvicorn/main.py

+11-13
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import time
1111
import typing
1212
from email.utils import formatdate
13-
from ipaddress import IPv4Address, IPv6Address, ip_address
1413

1514
import click
1615

@@ -25,7 +24,6 @@
2524
SSL_PROTOCOL_VERSION,
2625
WS_PROTOCOLS,
2726
Config,
28-
_get_server_start_message,
2927
)
3028
from uvicorn.supervisors import ChangeReload, Multiprocess
3129

@@ -506,6 +504,11 @@ def _share_socket(sock: socket) -> socket:
506504

507505
else:
508506
# Standard case. Create a socket from a host/port pair.
507+
addr_format = "%s://%s:%d"
508+
if config.host and ":" in config.host:
509+
# It's an IPv6 address.
510+
addr_format = "%s://[%s]:%d"
511+
509512
try:
510513
server = await loop.create_server(
511514
create_protocol,
@@ -522,17 +525,12 @@ def _share_socket(sock: socket) -> socket:
522525
if port == 0:
523526
port = server.sockets[0].getsockname()[1]
524527
protocol_name = "https" if config.ssl else "http"
525-
try:
526-
addr = ip_address(config.host)
527-
if isinstance(addr, IPv6Address):
528-
message, color_message = _get_server_start_message(
529-
is_ipv6_message=True
530-
)
531-
elif isinstance(addr, IPv4Address):
532-
message, color_message = _get_server_start_message()
533-
except ValueError:
534-
message, color_message = _get_server_start_message()
535-
528+
message = f"Uvicorn running on {addr_format} (Press CTRL+C to quit)"
529+
color_message = (
530+
"Uvicorn running on "
531+
+ click.style(addr_format, bold=True)
532+
+ " (Press CTRL+C to quit)"
533+
)
536534
logger.info(
537535
message,
538536
protocol_name,

0 commit comments

Comments
 (0)