mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-30 01:52:18 +00:00
fix(proxy): restructure UI before mounting; fall back to temp dir when read-only
When the packaged UI path is not writable (e.g., site-packages in a pip install), the previous code logged a warning and skipped restructuring, leaving login.html unreachable at /ui/login (404). The fix copies the UI to a writable temp directory and restructures there before mounting StaticFiles, so /ui/login resolves to login/index.html in all environments. The restructuring and temp-dir fallback now happen before app.mount() so the mounted directory is always in the correct state when the first request arrives. Fixes #29340
This commit is contained in:
parent
28c0d8579b
commit
d626e0a454
2 changed files with 66 additions and 26 deletions
|
|
@ -11,6 +11,7 @@ import secrets
|
|||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
|
|
@ -1631,21 +1632,6 @@ try:
|
|||
# Skip binary files or files we can't write to
|
||||
continue
|
||||
|
||||
# # Mount the _next directory at the root level
|
||||
app.mount(
|
||||
"/_next",
|
||||
StaticFiles(directory=os.path.join(ui_path, "_next")),
|
||||
name="next_static",
|
||||
)
|
||||
app.mount(
|
||||
f"{litellm_asset_prefix}/_next",
|
||||
StaticFiles(directory=os.path.join(ui_path, "_next")),
|
||||
name="next_static",
|
||||
)
|
||||
# print(f"mounted _next at {server_root_path}/ui/_next")
|
||||
|
||||
app.mount("/ui", StaticFiles(directory=ui_path, html=True), name="ui")
|
||||
|
||||
def _restructure_ui_html_files(ui_root: str) -> None:
|
||||
"""Ensure each exported HTML route is available as <route>/index.html."""
|
||||
|
||||
|
|
@ -1672,10 +1658,6 @@ try:
|
|||
# Another process may have already moved this file.
|
||||
continue
|
||||
|
||||
# Handle HTML file restructuring
|
||||
# Only restructure if:
|
||||
# 1. UI is not already pre-restructured
|
||||
# 2. Filesystem is writable
|
||||
try:
|
||||
is_pre_restructured = _is_ui_pre_restructured(ui_path)
|
||||
is_writable = os.access(ui_path, os.W_OK)
|
||||
|
|
@ -1684,15 +1666,17 @@ try:
|
|||
verbose_proxy_logger.info(
|
||||
f"Skipping UI restructuring: {ui_path} is already pre-restructured"
|
||||
)
|
||||
elif not is_writable:
|
||||
verbose_proxy_logger.warning(
|
||||
f"Cannot restructure UI at {ui_path}: path is not writable. "
|
||||
f"UI may not work correctly for extensionless routes. "
|
||||
f"Pre-build and restructure UI in Dockerfile for read-only deployments."
|
||||
)
|
||||
else:
|
||||
elif is_writable:
|
||||
_restructure_ui_html_files(ui_path)
|
||||
verbose_proxy_logger.info(f"Restructured UI directory: {ui_path}")
|
||||
else:
|
||||
tmp_dir = tempfile.mkdtemp(prefix="litellm_ui_")
|
||||
shutil.copytree(ui_path, tmp_dir, dirs_exist_ok=True)
|
||||
_restructure_ui_html_files(tmp_dir)
|
||||
ui_path = tmp_dir
|
||||
verbose_proxy_logger.info(
|
||||
f"Copied read-only UI to temp dir and restructured: {tmp_dir}"
|
||||
)
|
||||
except PermissionError as e:
|
||||
verbose_proxy_logger.exception(
|
||||
f"Permission error while restructuring UI directory {ui_path}: {e}"
|
||||
|
|
@ -1702,6 +1686,20 @@ try:
|
|||
f"Error while restructuring UI directory {ui_path}: {e}"
|
||||
)
|
||||
|
||||
# Mount _next and UI using the (possibly updated) ui_path
|
||||
app.mount(
|
||||
"/_next",
|
||||
StaticFiles(directory=os.path.join(ui_path, "_next")),
|
||||
name="next_static",
|
||||
)
|
||||
app.mount(
|
||||
f"{litellm_asset_prefix}/_next",
|
||||
StaticFiles(directory=os.path.join(ui_path, "_next")),
|
||||
name="next_static",
|
||||
)
|
||||
|
||||
app.mount("/ui", StaticFiles(directory=ui_path, html=True), name="ui")
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
|
|
|||
|
|
@ -604,6 +604,48 @@ def test_ui_extensionless_route_requires_restructure(tmp_path):
|
|||
assert "login" in response.text
|
||||
|
||||
|
||||
def test_read_only_ui_dir_falls_back_to_temp_dir_for_extensionless_routes(tmp_path):
|
||||
"""
|
||||
Regression for pip-install 404: when the UI directory cannot be restructured
|
||||
in-place (e.g., read-only site-packages), copying it to a writable temp dir
|
||||
and restructuring there must make /ui/login return 200.
|
||||
"""
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from litellm.proxy import proxy_server
|
||||
|
||||
ui_root = tmp_path / "ui"
|
||||
ui_root.mkdir()
|
||||
(ui_root / "index.html").write_text("<html>index</html>")
|
||||
(ui_root / "login.html").write_text("<html>login</html>")
|
||||
(ui_root / "_next").mkdir()
|
||||
|
||||
# Confirm that without restructuring the extensionless route returns 404.
|
||||
fastapi_before = FastAPI()
|
||||
fastapi_before.mount(
|
||||
"/ui", StaticFiles(directory=str(ui_root), html=True), name="ui"
|
||||
)
|
||||
assert TestClient(fastapi_before).get("/ui/login").status_code == 404
|
||||
|
||||
# Apply the read-only fallback: copy to a temp dir and restructure.
|
||||
tmp_dir = tempfile.mkdtemp(prefix="litellm_ui_test_")
|
||||
try:
|
||||
shutil.copytree(str(ui_root), tmp_dir, dirs_exist_ok=True)
|
||||
proxy_server._restructure_ui_html_files(tmp_dir)
|
||||
|
||||
assert (Path(tmp_dir) / "login" / "index.html").exists()
|
||||
assert not (Path(tmp_dir) / "login.html").exists()
|
||||
|
||||
fastapi_after = FastAPI()
|
||||
fastapi_after.mount("/ui", StaticFiles(directory=tmp_dir, html=True), name="ui")
|
||||
response = TestClient(fastapi_after).get("/ui/login")
|
||||
assert response.status_code == 200
|
||||
assert "login" in response.text
|
||||
finally:
|
||||
shutil.rmtree(tmp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
def test_admin_ui_export_serves_nested_extensionless_routes():
|
||||
out_dir = Path(litellm.__file__).parent / "proxy" / "_experimental" / "out"
|
||||
assert out_dir.is_dir(), f"missing UI export at {out_dir}"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue