server: listen on both loopback families for localhost.
Bind sockets ourselves and pass them to uvicorn (Server.serve(sockets) or the ChangeReload/Multiprocess supervisors, same API uvicorn.run uses). localhost expands to 127.0.0.1 and ::1 so clients reach the server regardless of how localhost resolves; individual bind failures degrade with a warning. Reload mode now serves all endpoints instead of only the first, multi-endpoint configs no longer run lifespan twice, and unix sockets are cleaned up on exit.
This commit is contained in:
@@ -57,7 +57,7 @@ Dev-mode behavior with `FastAPI(debug=True)`: requests error HTTP 409 with a mes
|
||||
|
||||
## Server runner
|
||||
|
||||
When you need more flexibility than `fastapi` CLI can provide (e.g. CLI arguments to your own program), you may use this convenience to run FastAPI app with Uvicorn startup on given `listen` endpoints. Runs in the same process if possible but delegates to `uvicorn.run()` for auto-reloads and multiple workers. This would typically be called from your CLI main, which can set its own env variables to pass information to the FastAPI instances that run (Python imports only work in same-process mode).
|
||||
When you need more flexibility than `fastapi` CLI can provide (e.g. CLI arguments to your own program), you may use this convenience to run FastAPI app with Uvicorn startup on given `listen` endpoints. Runs in the same process if possible but delegates to uvicorn supervisors for auto-reloads and multiple workers. `localhost` binds both 127.0.0.1 and ::1, so the server stays reachable regardless of how the name resolves. This would typically be called from your CLI main, which can set its own env variables to pass information to the FastAPI instances that run (Python imports only work in same-process mode).
|
||||
|
||||
```python
|
||||
from fastapi_vue import server
|
||||
|
||||
@@ -4,6 +4,7 @@ import asyncio
|
||||
import importlib.metadata
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -11,6 +12,8 @@ from typing import Any
|
||||
import tracerite
|
||||
import uvicorn
|
||||
from uvicorn import Config, Server
|
||||
from uvicorn.main import STARTUP_FAILURE
|
||||
from uvicorn.supervisors import ChangeReload, Multiprocess
|
||||
|
||||
from .hostutil import parse_endpoints
|
||||
from .logging import (
|
||||
@@ -143,6 +146,68 @@ def run( # noqa: PLR0913
|
||||
asyncio.run(serve(endpoints, **conf))
|
||||
|
||||
|
||||
def _bind_sockets(endpoints: list[dict]) -> list[socket.socket]:
|
||||
"""Bind sockets for all endpoints, expanding localhost to both loopbacks.
|
||||
|
||||
localhost is bound as 127.0.0.1 and ::1 explicitly, so resolver quirks
|
||||
(notably Windows resolving localhost to ::1 only) cannot make the server
|
||||
unreachable. Addresses that cannot be bound (e.g. IPv6 unavailable) are
|
||||
skipped with a warning; exits only if nothing could be bound.
|
||||
"""
|
||||
sockets: list[socket.socket] = []
|
||||
seen: set = set()
|
||||
for ep in endpoints:
|
||||
if "uds" in ep:
|
||||
uds = ep["uds"]
|
||||
if uds in seen:
|
||||
continue
|
||||
seen.add(uds)
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
try:
|
||||
sock.bind(uds)
|
||||
Path(uds).chmod(0o666)
|
||||
except OSError as e:
|
||||
logger.warning("Could not bind unix socket %s: %s", uds, e)
|
||||
sock.close()
|
||||
continue
|
||||
sock.set_inheritable(True)
|
||||
sockets.append(sock)
|
||||
continue
|
||||
|
||||
host, port = ep["host"], ep["port"]
|
||||
hosts = ("127.0.0.1", "::1") if host == "localhost" else (host,)
|
||||
for addr in hosts:
|
||||
if (addr, port) in seen:
|
||||
continue
|
||||
seen.add((addr, port))
|
||||
family = socket.AF_INET6 if ":" in addr else socket.AF_INET
|
||||
sock = socket.socket(family, socket.SOCK_STREAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
if family == socket.AF_INET6:
|
||||
with suppress(OSError):
|
||||
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
|
||||
try:
|
||||
sock.bind((addr, port))
|
||||
except OSError as e:
|
||||
logger.warning("Could not bind %s:%d: %s", addr, port, e)
|
||||
sock.close()
|
||||
continue
|
||||
sock.set_inheritable(True)
|
||||
sockets.append(sock)
|
||||
|
||||
if not sockets:
|
||||
logger.error("Could not bind any endpoint")
|
||||
raise SystemExit(STARTUP_FAILURE)
|
||||
return sockets
|
||||
|
||||
|
||||
def _remove_uds_files(endpoints: list[dict]) -> None:
|
||||
"""Remove unix socket files we created (mirrors uvicorn.run cleanup)."""
|
||||
for ep in endpoints:
|
||||
if "uds" in ep:
|
||||
Path(ep["uds"]).unlink(missing_ok=True)
|
||||
|
||||
|
||||
async def serve(endpoints: list[dict], **kwargs: Any) -> None: # noqa: ANN401
|
||||
"""Serve the given endpoints in current process/loop. Does not spawn extra processes."""
|
||||
forbidden = {"reload", "workers"} & {k for k, v in kwargs.items() if v}
|
||||
@@ -151,16 +216,21 @@ async def serve(endpoints: list[dict], **kwargs: Any) -> None: # noqa: ANN401
|
||||
"Options %s have no effect in simple mode (multiple endpoints)",
|
||||
", ".join(sorted(forbidden)),
|
||||
)
|
||||
await asyncio.gather(*(Server(Config(**kwargs, **ep)).serve() for ep in endpoints))
|
||||
try:
|
||||
await Server(Config(**kwargs)).serve(sockets=_bind_sockets(endpoints))
|
||||
finally:
|
||||
_remove_uds_files(endpoints)
|
||||
|
||||
|
||||
def serve_multiprocess(endpoints: list[dict], **kwargs: Any) -> None: # noqa: ANN401
|
||||
"""Serve using uvicorn.run() for reload/workers support. Only first endpoint is used."""
|
||||
if len(endpoints) > 1:
|
||||
eps = [ep["uds"] if "uds" in ep else f"{ep['host']}:{ep['port']}" for ep in endpoints]
|
||||
logger.warning(
|
||||
"Current mode supports only one endpoint. Listening: %s, skipped: %s",
|
||||
eps[0],
|
||||
" ".join(eps[1:]),
|
||||
)
|
||||
uvicorn.run(**kwargs, **endpoints[0])
|
||||
"""Serve using uvicorn supervisors for reload/workers support."""
|
||||
config = Config(**kwargs)
|
||||
server = Server(config)
|
||||
sockets = _bind_sockets(endpoints)
|
||||
try:
|
||||
if config.should_reload:
|
||||
ChangeReload(config, target=server.run, sockets=sockets).run()
|
||||
else:
|
||||
Multiprocess(config, sockets=sockets).run()
|
||||
finally:
|
||||
_remove_uds_files(endpoints)
|
||||
|
||||
Reference in New Issue
Block a user