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(config): Add asgi-root-path option #2493

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
8 changes: 6 additions & 2 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,12 @@ Options:
to the $FORWARDED_ALLOW_IPS environment
variable if available, or '127.0.0.1'. The
literal '*' means trust everything.
--root-path TEXT Set the ASGI 'root_path' for applications
submounted below a given URL path.
--root-path TEXT Serve the application under the provided
root path.
--asgi-root-path TEXT Set the ASGI 'root_path' for applications
submounted below a given URL path. This is
useful for applications served on a sub-URL,
such as behind a reverse proxy.
--limit-concurrency INTEGER Maximum number of concurrent connections or
tasks to allow, before issuing HTTP 503
responses.
Expand Down
8 changes: 6 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,12 @@ Options:
to the $FORWARDED_ALLOW_IPS environment
variable if available, or '127.0.0.1'. The
literal '*' means trust everything.
--root-path TEXT Set the ASGI 'root_path' for applications
submounted below a given URL path.
--root-path TEXT Serve the application under the provided
root path.
--asgi-root-path TEXT Set the ASGI 'root_path' for applications
submounted below a given URL path. This is
useful for applications served on a sub-URL,
such as behind a reverse proxy.
--limit-concurrency INTEGER Maximum number of concurrent connections or
tasks to allow, before issuing HTTP 503
responses.
Expand Down
3 changes: 2 additions & 1 deletion docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ Note that WSGI mode always disables WebSocket support, as it is not supported by

## HTTP

* `--root-path <str>` - Set the ASGI `root_path` for applications submounted below a given URL path.
* `--root-path <str>` - Serve the application under the provided root path (mount point).
* `--asgi-root-path <str>` - Set the ASGI 'root_path' for applications served behind a proxy in a subpath (mutually exclusive with `root-path`)
* `--proxy-headers` / `--no-proxy-headers` - Enable/Disable X-Forwarded-Proto, X-Forwarded-For to populate remote address info. Defaults to enabled, but is restricted to only trusting
connecting IPs in the `forwarded-allow-ips` configuration.
* `--forwarded-allow-ips` <comma-separated-list> Comma separated list of IP Addresses, IP Networks, or literals (e.g. UNIX Socket path) to trust with proxy headers. Defaults to the `$FORWARDED_ALLOW_IPS` environment variable if available, or '127.0.0.1'. The literal `'*'` means trust everything.
Expand Down
23 changes: 23 additions & 0 deletions tests/protocols/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,29 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable
assert b"root_path=/app path=/app/" in protocol.transport.buffer


async def test_asgi_root_path(http_protocol_cls: HTTPProtocol):
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
assert scope["type"] == "http"
root_path = scope.get("root_path", "")
path = scope["path"]
response = Response(f"root_path={root_path} path={path}", media_type="text/plain")
await response(scope, receive, send)

protocol = get_connected_protocol(app, http_protocol_cls, asgi_root_path="/one")
protocol.data_received(GET_REQUEST_WITH_RAW_PATH)
await protocol.loop.run_one()
assert b"HTTP/1.1 200 OK" in protocol.transport.buffer
assert b"root_path=/one path=/one/two" in protocol.transport.buffer

# This is a misconfiguration, but it helps us confirm that asgi_root_path
# doesn't prefix the path like root_path does.
protocol = get_connected_protocol(app, http_protocol_cls, asgi_root_path="/unrelated")
protocol.data_received(GET_REQUEST_WITH_RAW_PATH)
await protocol.loop.run_one()
assert b"HTTP/1.1 200 OK" in protocol.transport.buffer
assert b"root_path=/unrelated path=/one/two" in protocol.transport.buffer


async def test_raw_path(http_protocol_cls: HTTPProtocol):
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
assert scope["type"] == "http"
Expand Down
5 changes: 5 additions & 0 deletions uvicorn/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ def __init__(
date_header: bool = True,
forwarded_allow_ips: list[str] | str | None = None,
root_path: str = "",
asgi_root_path: str = "",
limit_concurrency: int | None = None,
limit_max_requests: int | None = None,
backlog: int = 2048,
Expand Down Expand Up @@ -250,6 +251,7 @@ def __init__(
self.server_header = server_header
self.date_header = date_header
self.root_path = root_path
self.asgi_root_path = asgi_root_path or root_path
self.limit_concurrency = limit_concurrency
self.limit_max_requests = limit_max_requests
self.backlog = backlog
Expand Down Expand Up @@ -277,6 +279,9 @@ def __init__(
self.reload_includes: list[str] = []
self.reload_excludes: list[str] = []

if root_path and asgi_root_path:
logger.error("Setting both 'root_path' and 'asgi_root_path' is not supported.")

if (reload_dirs or reload_includes or reload_excludes) and not self.should_reload:
logger.warning(
"Current configuration will not reload as not all conditions are met, " "please refer to documentation."
Expand Down
17 changes: 16 additions & 1 deletion uvicorn/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,14 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
"--root-path",
type=str,
default="",
help="Set the ASGI 'root_path' for applications submounted below a given URL path.",
help="Serve the application under the provided root path.",
)
@click.option(
"--asgi-root-path",
type=str,
default="",
help="Set the ASGI 'root_path' for applications submounted below a given URL path. "
"This is useful for applications served on a sub-URL, such as behind a reverse proxy.",
)
@click.option(
"--limit-concurrency",
Expand Down Expand Up @@ -391,6 +398,7 @@ def main(
date_header: bool,
forwarded_allow_ips: str,
root_path: str,
asgi_root_path: str,
limit_concurrency: int,
backlog: int,
limit_max_requests: int,
Expand Down Expand Up @@ -440,6 +448,7 @@ def main(
date_header=date_header,
forwarded_allow_ips=forwarded_allow_ips,
root_path=root_path,
asgi_root_path=asgi_root_path,
limit_concurrency=limit_concurrency,
backlog=backlog,
limit_max_requests=limit_max_requests,
Expand Down Expand Up @@ -492,6 +501,7 @@ def run(
date_header: bool = True,
forwarded_allow_ips: list[str] | str | None = None,
root_path: str = "",
asgi_root_path: str = "",
limit_concurrency: int | None = None,
backlog: int = 2048,
limit_max_requests: int | None = None,
Expand Down Expand Up @@ -544,6 +554,7 @@ def run(
date_header=date_header,
forwarded_allow_ips=forwarded_allow_ips,
root_path=root_path,
asgi_root_path=asgi_root_path,
limit_concurrency=limit_concurrency,
backlog=backlog,
limit_max_requests=limit_max_requests,
Expand All @@ -568,6 +579,10 @@ def run(
logger.warning("You must pass the application as an import string to enable 'reload' or " "'workers'.")
sys.exit(1)

if root_path and asgi_root_path:
# Config did already log an error. Just quit.
sys.exit(1)

try:
if config.should_reload:
sock = config.bind_socket()
Expand Down
3 changes: 2 additions & 1 deletion uvicorn/protocols/http/h11_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def __init__(
)
self.ws_protocol_class = config.ws_protocol_class
self.root_path = config.root_path
self.asgi_root_path = config.asgi_root_path
self.limit_concurrency = config.limit_concurrency
self.app_state = app_state

Expand Down Expand Up @@ -209,7 +210,7 @@ def handle_events(self) -> None:
"client": self.client,
"scheme": self.scheme, # type: ignore[typeddict-item]
"method": event.method.decode("ascii"),
"root_path": self.root_path,
"root_path": self.asgi_root_path,
"path": full_path,
"raw_path": full_raw_path,
"query_string": query_string,
Expand Down
3 changes: 2 additions & 1 deletion uvicorn/protocols/http/httptools_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def __init__(
self.parser = httptools.HttpRequestParser(self)
self.ws_protocol_class = config.ws_protocol_class
self.root_path = config.root_path
self.asgi_root_path = config.asgi_root_path
self.limit_concurrency = config.limit_concurrency
self.app_state = app_state

Expand Down Expand Up @@ -219,7 +220,7 @@ def on_message_begin(self) -> None:
"server": self.server,
"client": self.client,
"scheme": self.scheme, # type: ignore[typeddict-item]
"root_path": self.root_path,
"root_path": self.asgi_root_path,
"headers": self.headers,
"state": self.app_state.copy(),
}
Expand Down
Loading