mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
fix(gateway): keep prometheus /metrics mount when trimming gateway routes
The componentized gateway trims the shared proxy route table to the data-plane allowlist, but _is_gateway_route dropped every Mount unconditionally. Prometheus registers /metrics as a Mount (make_asgi_app), not an APIRoute, so the gateway returned 404 on /metrics even though gateway/routes/allowlist.py lists it. Add a GATEWAY_MOUNT_PATHS allowlist (mirroring the BACKEND_MOUNT_PATHS pattern) holding /metrics, and keep a Mount only when its path is in it. The UI static mounts and the MCP catch-all Mount stay dropped. Fixes #30291
This commit is contained in:
parent
c90eb7e96f
commit
ae10154f78
3 changed files with 61 additions and 4 deletions
|
|
@ -25,7 +25,11 @@ DatabaseURLSettings.from_env().apply_to_env()
|
|||
|
||||
from litellm.proxy.proxy_server import app
|
||||
|
||||
from gateway.routes.allowlist import GATEWAY_EXACT_PATHS, GATEWAY_PATH_PREFIXES
|
||||
from gateway.routes.allowlist import (
|
||||
GATEWAY_EXACT_PATHS,
|
||||
GATEWAY_MOUNT_PATHS,
|
||||
GATEWAY_PATH_PREFIXES,
|
||||
)
|
||||
|
||||
|
||||
def _is_gateway_route(route) -> bool:
|
||||
|
|
@ -34,8 +38,10 @@ def _is_gateway_route(route) -> bool:
|
|||
if path is None:
|
||||
return False
|
||||
if isinstance(route, Mount):
|
||||
# Gateway never serves the static UI or its asset bundles.
|
||||
return False
|
||||
# The static UI mounts are served by the dedicated UI container. Only
|
||||
# Mounts in the gateway allowlist (e.g. the Prometheus /metrics scrape,
|
||||
# registered via make_asgi_app) remain on the gateway.
|
||||
return path in GATEWAY_MOUNT_PATHS
|
||||
if path in GATEWAY_EXACT_PATHS:
|
||||
return True
|
||||
return any(path.startswith(prefix) for prefix in GATEWAY_PATH_PREFIXES)
|
||||
|
|
|
|||
|
|
@ -120,3 +120,9 @@ GATEWAY_EXACT_PATHS: frozenset[str] = frozenset(
|
|||
"/test",
|
||||
}
|
||||
)
|
||||
|
||||
GATEWAY_MOUNT_PATHS: frozenset[str] = frozenset(
|
||||
{
|
||||
"/metrics", # Prometheus scrape endpoint, registered as an ASGI Mount
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -47,9 +47,20 @@ from backend.routes.allowlist import (
|
|||
BACKEND_MOUNT_PATHS,
|
||||
BACKEND_PATH_PREFIXES,
|
||||
)
|
||||
from gateway.routes.allowlist import GATEWAY_EXACT_PATHS, GATEWAY_PATH_PREFIXES
|
||||
from gateway.routes.allowlist import (
|
||||
GATEWAY_EXACT_PATHS,
|
||||
GATEWAY_MOUNT_PATHS,
|
||||
GATEWAY_PATH_PREFIXES,
|
||||
)
|
||||
from litellm.proxy.proxy_server import app
|
||||
|
||||
# Importing gateway.main wraps the shared app's lifespan_context; save and
|
||||
# restore it so the wrapper does not leak into the other tests in this module.
|
||||
_PRE_IMPORT_LIFESPAN = app.router.lifespan_context
|
||||
from gateway.main import _is_gateway_route
|
||||
|
||||
app.router.lifespan_context = _PRE_IMPORT_LIFESPAN
|
||||
|
||||
for _key, _previous in _PRE_EXISTING_ENV.items():
|
||||
if _previous is None:
|
||||
os.environ.pop(_key, None)
|
||||
|
|
@ -133,3 +144,37 @@ def test_backend_drops_non_allowlisted_mounts():
|
|||
for mount_path in non_backend_mounts:
|
||||
assert mount_path not in BACKEND_MOUNT_PATHS, \
|
||||
f"Mount {mount_path} should not be in BACKEND_MOUNT_PATHS"
|
||||
|
||||
|
||||
def test_gateway_mount_paths_defined():
|
||||
"""GATEWAY_MOUNT_PATHS constant must exist and be a frozenset."""
|
||||
assert isinstance(GATEWAY_MOUNT_PATHS, frozenset), \
|
||||
f"GATEWAY_MOUNT_PATHS must be a frozenset, got {type(GATEWAY_MOUNT_PATHS)}"
|
||||
assert len(GATEWAY_MOUNT_PATHS) > 0, \
|
||||
"GATEWAY_MOUNT_PATHS must contain at least one Mount path"
|
||||
|
||||
|
||||
def test_metrics_mount_in_gateway_allowlist():
|
||||
"""The /metrics Mount must be in GATEWAY_MOUNT_PATHS."""
|
||||
assert "/metrics" in GATEWAY_MOUNT_PATHS, \
|
||||
"/metrics Mount path must be in GATEWAY_MOUNT_PATHS"
|
||||
|
||||
|
||||
async def _asgi_noop(scope, receive, send) -> None:
|
||||
return None
|
||||
|
||||
|
||||
def test_gateway_keeps_prometheus_metrics_mount():
|
||||
"""Regression for #30291.
|
||||
|
||||
Prometheus registers /metrics as a Mount (``make_asgi_app``), not an
|
||||
APIRoute. The gateway trim used to drop every Mount, so /metrics returned
|
||||
404 on ``gateway.main`` even though the allowlist lists it.
|
||||
"""
|
||||
assert _is_gateway_route(Mount("/metrics", app=_asgi_noop)) is True
|
||||
|
||||
|
||||
def test_gateway_drops_non_allowlisted_mounts():
|
||||
"""Keeping /metrics must not leak the UI static or MCP catch-all Mounts."""
|
||||
for path in ("/", "/ui", "/_next", "/swagger", "/mcp", "/sse"):
|
||||
assert _is_gateway_route(Mount(path, app=_asgi_noop)) is False, path
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue