Source code for fluxlit.gateway.paths
"""Public path prefix and API mount routing helpers."""
from __future__ import annotations
from starlette.types import Scope
[docs]
def normalize_root_mount(raw: str) -> str:
"""Normalize a public URL prefix (e.g. Posit Connect content path) for routing.
Returns ``""`` when unset, otherwise a path starting with ``/`` and no trailing
slash (except root ``"/"`` is not used — empty means no mount).
"""
s = (raw or "").strip()
if not s:
return ""
if not s.startswith("/"):
s = f"/{s}"
return s.rstrip("/") or ""
[docs]
def split_gateway_paths(path: str, root_mount: str) -> tuple[str, str]:
"""Split the ASGI path for dispatch vs Streamlit upstream.
Some reverse proxies forward the **full** public path (``/content/123/api/...``).
Others strip the mount and only forward the suffix (``/api/...``) while setting
ASGI ``root_path``. :func:`normalize_root_mount` should match the browser-visible
prefix configured for Streamlit ``server.baseUrlPath``.
Returns:
``(dispatch_path, streamlit_path)`` — use *dispatch_path* to choose API vs
Streamlit; send *streamlit_path* to the Streamlit sidecar when proxying.
"""
m = normalize_root_mount(root_mount)
p = path if path.startswith("/") else f"/{path}"
if not m:
return p, p
if p == m or p.startswith(f"{m}/"):
rest = "/" if p == m else p[len(m) :]
if not rest.startswith("/"):
rest = f"/{rest}"
return rest, p
return p, f"{m}{p}"
def location_under_mount(mount: str, suffix: str) -> str:
"""Build a root-absolute Location (``/app/api/docs``) when mounted under ``/app``."""
m = normalize_root_mount(mount)
s = suffix if suffix.startswith("/") else f"/{suffix}"
return f"{m}{s}" if m else s
def strip_prefix_scope(scope: Scope, prefix: str) -> Scope:
"""Strip ``prefix`` from the URL path and extend ``root_path`` (ASGI).
FastAPI's Swagger / ReDoc handlers build ``openapi_url`` as
``scope["root_path"] + app.openapi_url``. Without setting ``root_path`` to the
gateway's API mount (e.g. ``/api``), the embedded URL is ``/openapi.json``; the
browser then requests the **gateway** root, which is proxied to Streamlit — not
JSON — and Swagger UI reports a missing OpenAPI version.
"""
path = scope.get("path") or "/"
rest = path.removeprefix(prefix) or "/"
new_scope: Scope = dict(scope)
new_scope["path"] = rest
new_scope["raw_path"] = rest.encode("latin-1")
prior = (scope.get("root_path") or "").rstrip("/")
mount = prefix.rstrip("/")
new_scope["root_path"] = f"{prior}{mount}" if prior else mount
return new_scope