Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: supports setting multiple hosts #2486

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ $ uvicorn --help
Usage: uvicorn [OPTIONS] APP

Options:
--host TEXT Bind socket to this host. [default:
127.0.0.1]
--host TEXT Bind socket to this host.
vvanglro marked this conversation as resolved.
Show resolved Hide resolved
--port INTEGER Bind socket to this port. If 0, an available
port will be picked. [default: 8000]
--uds TEXT Bind to a UNIX domain socket.
Expand Down
3 changes: 1 addition & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,7 @@ $ uvicorn --help
Usage: uvicorn [OPTIONS] APP

Options:
--host TEXT Bind socket to this host. [default:
127.0.0.1]
--host TEXT Bind socket to this host.
--port INTEGER Bind socket to this port. If 0, an available
port will be picked. [default: 8000]
--uds TEXT Bind to a UNIX domain socket.
Expand Down
3 changes: 1 addition & 2 deletions docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ For example, in case you want to run the app on port `5000`, just set the enviro

## Socket Binding

* `--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'*.
* `--port <int>` - Bind to a socket with this port. **Default:** *8000*.
* `--host <str>` - Bind socket to this host. May be used multiple times. If unused, then by default 127.0.0.1. IPv6 addresses are supported, for example: `--host '::'`, when using ipv6 only, if ipv4 is available it will work at the same time.
* `--uds <path>` - Bind to a UNIX domain socket, for example `--uds /tmp/uvicorn.sock`. Useful if you want to run Uvicorn behind a reverse proxy.
* `--fd <int>` - Bind to socket from this file descriptor. Useful if you want to run Uvicorn within a process manager.

Expand Down
31 changes: 17 additions & 14 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,9 +262,10 @@ def test_concrete_http_class() -> None:
def test_socket_bind() -> None:
config = Config(app=asgi_app)
config.load()
sock = config.bind_socket()
assert isinstance(sock, socket.socket)
sock.close()
sockets = config.bind_socket()
for sock in sockets:
assert isinstance(sock, socket.socket)
sock.close()


def test_ssl_config(
Expand Down Expand Up @@ -493,11 +494,12 @@ def test_bind_unix_socket_works_with_reload_or_workers(
): # pragma: py-win32
config = Config(app=asgi_app, uds=short_socket_name, reload=reload, workers=workers)
config.load()
sock = config.bind_socket()
assert isinstance(sock, socket.socket)
assert sock.family == socket.AF_UNIX
assert sock.getsockname() == short_socket_name
sock.close()
sockets = config.bind_socket()
for sock in sockets:
assert isinstance(sock, socket.socket)
assert sock.family == socket.AF_UNIX
assert sock.getsockname() == short_socket_name
sock.close()


@pytest.mark.parametrize(
Expand All @@ -514,12 +516,13 @@ def test_bind_fd_works_with_reload_or_workers(reload: bool, workers: int): # pr
fd = fdsock.fileno()
config = Config(app=asgi_app, fd=fd, reload=reload, workers=workers)
config.load()
sock = config.bind_socket()
assert isinstance(sock, socket.socket)
assert sock.family == socket.AF_UNIX
assert sock.getsockname() == ""
sock.close()
fdsock.close()
sockets = config.bind_socket()
for sock in sockets:
assert isinstance(sock, socket.socket)
assert sock.family == socket.AF_UNIX
assert sock.getsockname() == ""
sock.close()
fdsock.close()


@pytest.mark.parametrize(
Expand Down
59 changes: 34 additions & 25 deletions uvicorn/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ class Config:
def __init__(
self,
app: ASGIApplication | Callable[..., Any] | str,
host: str = "127.0.0.1",
host: list[str] | str = "127.0.0.1",
port: int = 8000,
uds: str | None = None,
fd: int | None = None,
Expand Down Expand Up @@ -225,6 +225,8 @@ def __init__(
h11_max_incomplete_event_size: int | None = None,
):
self.app = app
if host and isinstance(host, str):
host = [host]
self.host = host
self.port = port
self.uds = uds
Expand Down Expand Up @@ -476,7 +478,8 @@ def setup_event_loop(self) -> None:
if loop_setup is not None:
loop_setup(use_subprocess=self.use_subprocess)

def bind_socket(self) -> socket.socket:
def bind_socket(self) -> list[socket.socket]:
sockets: list[socket.socket] = []
logger_args: list[str | int]
if self.uds: # pragma: py-win32
path = self.uds
Expand All @@ -489,40 +492,46 @@ def bind_socket(self) -> socket.socket:
logger.error(exc)
sys.exit(1)

sockets.append(sock)
message = "Uvicorn running on unix socket %s (Press CTRL+C to quit)"
sock_name_format = "%s"
color_message = "Uvicorn running on " + click.style(sock_name_format, bold=True) + " (Press CTRL+C to quit)"
logger_args = [self.uds]
logger.info(message, *logger_args, extra={"color_message": color_message})
elif self.fd: # pragma: py-win32
sock = socket.fromfd(self.fd, socket.AF_UNIX, socket.SOCK_STREAM)
sockets.append(sock)
message = "Uvicorn running on socket %s (Press CTRL+C to quit)"
fd_name_format = "%s"
color_message = "Uvicorn running on " + click.style(fd_name_format, bold=True) + " (Press CTRL+C to quit)"
logger_args = [sock.getsockname()]
logger.info(message, *logger_args, extra={"color_message": color_message})
else:
family = socket.AF_INET
addr_format = "%s://%s:%d"

if self.host and ":" in self.host: # pragma: full coverage
# 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))
except OSError as exc: # pragma: full coverage
logger.error(exc)
sys.exit(1)

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_args = [protocol_name, self.host, sock.getsockname()[1]]
logger.info(message, *logger_args, extra={"color_message": color_message})
sock.set_inheritable(True)
return sock
for host in self.host:
family = socket.AF_INET
addr_format = "%s://%s:%d"

if ":" in host: # pragma: full coverage
# 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((host, self.port))
except OSError as exc: # pragma: full coverage
logger.error(exc)
sys.exit(1)
sockets.append(sock)
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_args = [protocol_name, host, sock.getsockname()[1]]
logger.info(message, *logger_args, extra={"color_message": color_message})
for sock in sockets:
sock.set_inheritable(True)
return sockets

@property
def should_reload(self) -> bool:
Expand Down
19 changes: 10 additions & 9 deletions uvicorn/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,11 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
@click.argument("app", envvar="UVICORN_APP")
@click.option(
"--host",
type=str,
default="127.0.0.1",
multiple=True,
help="Bind socket to this host.",
show_default=True,
default=[
"127.0.0.1",
],
)
@click.option(
"--port",
Expand Down Expand Up @@ -362,7 +363,7 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
)
def main(
app: str,
host: str,
host: list[str],
port: int,
uds: str,
fd: int,
Expand Down Expand Up @@ -463,7 +464,7 @@ def main(
def run(
app: ASGIApplication | Callable[..., Any] | str,
*,
host: str = "127.0.0.1",
host: list[str] | str = "127.0.0.1",
port: int = 8000,
uds: str | None = None,
fd: int | None = None,
Expand Down Expand Up @@ -570,11 +571,11 @@ def run(

try:
if config.should_reload:
sock = config.bind_socket()
ChangeReload(config, target=server.run, sockets=[sock]).run()
sockets = config.bind_socket()
ChangeReload(config, target=server.run, sockets=sockets).run()
elif config.workers > 1:
sock = config.bind_socket()
Multiprocess(config, target=server.run, sockets=[sock]).run()
sockets = config.bind_socket()
Multiprocess(config, target=server.run, sockets=sockets).run()
else:
server.run()
except KeyboardInterrupt:
Expand Down
40 changes: 20 additions & 20 deletions uvicorn/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,26 +198,26 @@ def _log_started_message(self, listeners: Sequence[socket.SocketType]) -> None:
logger.info("Uvicorn running on unix socket %s (Press CTRL+C to quit)", config.uds)

else:
addr_format = "%s://%s:%d"
host = "0.0.0.0" if config.host is None else config.host
if ":" in host:
# It's an IPv6 address.
addr_format = "%s://[%s]:%d"

port = config.port
if port == 0:
port = listeners[0].getsockname()[1]

protocol_name = "https" if config.ssl else "http"
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,
host,
port,
extra={"color_message": color_message},
)
for host in config.host:
addr_format = "%s://%s:%d"
if ":" in host:
# It's an IPv6 address.
addr_format = "%s://[%s]:%d"

port = config.port
if port == 0:
port = listeners[0].getsockname()[1]

protocol_name = "https" if config.ssl else "http"
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,
host,
port,
extra={"color_message": color_message},
)

async def main_loop(self) -> None:
counter = 0
Expand Down
Loading