mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-25 01:02:15 +00:00
fix(proxy/ui): detect stale restructured dirs as needing rebuild
`_is_ui_pre_restructured` returned True as soon as it found any `<route>/index.html` subdirectory, even when a fresh `<route>.html` source file was sitting alongside it. That mixed state happens when the Next.js build output is updated (new chunk hashes) while leftover restructured directories from a previous proxy run remain on disk — e.g. after `git checkout` brings back the new `<route>.html`. The detection short-circuited, the restructure step was skipped, and the proxy served the stale `<route>/index.html` referencing chunks that no longer exist (404 on `/_next/static/chunks/<hash>.js`). Walk the tree in the fallback signal: only return True when at least one restructured subdirectory exists AND no unrestructured `*.html` source files remain (outside `_next/` and `litellm-asset-prefix/`). Mixed state now correctly triggers a re-restructure, which atomically overwrites the stale `index.html` files via `os.replace`. Adds regression tests covering the four states: marker file, clean pattern, fully unrestructured, and mixed.
This commit is contained in:
parent
e58a561caa
commit
2cc5177401
2 changed files with 89 additions and 11 deletions
|
|
@ -1313,6 +1313,12 @@ try:
|
|||
Returns True if:
|
||||
1. Marker file .litellm_ui_ready exists (created by Dockerfile), OR
|
||||
2. Restructuring pattern detected (subdirectories with index.html inside)
|
||||
AND no unrestructured source *.html files remain alongside them.
|
||||
|
||||
Mixed state — a fresh source `<route>.html` next to a stale
|
||||
`<route>/index.html` from a previous run — must NOT return True. The
|
||||
stale directory's HTML may reference asset chunks that no longer exist
|
||||
in a newer build, producing 404s on `/_next/static/chunks/...`.
|
||||
|
||||
This allows skipping copy/restructure operations on read-only filesystems.
|
||||
"""
|
||||
|
|
@ -1332,26 +1338,44 @@ try:
|
|||
if not os.path.exists(os.path.join(ui_dir, "index.html")):
|
||||
return False
|
||||
|
||||
# Look for ANY subdirectory with index.html (proves restructuring happened)
|
||||
# Ignore directories starting with _ (Next.js internals like _next)
|
||||
# Walk the tree to confirm both:
|
||||
# (a) at least one restructured subdirectory exists, and
|
||||
# (b) no unrestructured *.html source files remain anywhere.
|
||||
# If we find any non-index *.html (outside Next.js asset dirs) we
|
||||
# treat the tree as needing restructure — even if some routes look
|
||||
# already converted, those converted directories may be stale.
|
||||
has_restructured_dir = False
|
||||
try:
|
||||
for entry in os.scandir(ui_dir):
|
||||
if entry.is_dir() and not entry.name.startswith("_"):
|
||||
index_path = os.path.join(entry.path, "index.html")
|
||||
if os.path.exists(index_path):
|
||||
# Found at least one restructured route - this proves the pattern
|
||||
for current_root, dirs, files in os.walk(ui_dir):
|
||||
if current_root == ui_dir:
|
||||
# Prune Next.js asset directories from the walk; their
|
||||
# internal *.html files (if any) are not route sources.
|
||||
dirs[:] = [
|
||||
d for d in dirs if d not in {"_next", "litellm-asset-prefix"}
|
||||
]
|
||||
|
||||
if current_root != ui_dir and "index.html" in files:
|
||||
has_restructured_dir = True
|
||||
|
||||
for filename in files:
|
||||
if filename.endswith(".html") and filename != "index.html":
|
||||
verbose_proxy_logger.debug(
|
||||
f"Detected restructured UI via pattern: found {entry.name}/index.html"
|
||||
f"Found unrestructured HTML file at "
|
||||
f"{os.path.join(current_root, filename)} — "
|
||||
f"UI is not pre-restructured"
|
||||
)
|
||||
return True
|
||||
return False
|
||||
except (PermissionError, OSError) as e:
|
||||
verbose_proxy_logger.debug(
|
||||
f"Could not scan {ui_dir} for restructuring detection: {e}"
|
||||
)
|
||||
return False
|
||||
|
||||
# No restructured routes found
|
||||
return False
|
||||
if has_restructured_dir:
|
||||
verbose_proxy_logger.debug(
|
||||
f"Detected restructured UI via pattern in {ui_dir}"
|
||||
)
|
||||
return has_restructured_dir
|
||||
|
||||
def _try_populate_ui_directory(
|
||||
source_path: str, target_path: str
|
||||
|
|
|
|||
|
|
@ -604,6 +604,60 @@ def test_ui_extensionless_route_requires_restructure(tmp_path):
|
|||
assert "login" in response.text
|
||||
|
||||
|
||||
def test_is_ui_pre_restructured_marker_file(tmp_path):
|
||||
from litellm.proxy import proxy_server
|
||||
|
||||
ui_root = tmp_path / "ui"
|
||||
ui_root.mkdir()
|
||||
(ui_root / ".litellm_ui_ready").write_text("")
|
||||
|
||||
assert proxy_server._is_ui_pre_restructured(str(ui_root)) is True
|
||||
|
||||
|
||||
def test_is_ui_pre_restructured_clean_pattern(tmp_path):
|
||||
from litellm.proxy import proxy_server
|
||||
|
||||
ui_root = tmp_path / "ui"
|
||||
ui_root.mkdir()
|
||||
(ui_root / "index.html").write_text("root")
|
||||
(ui_root / "login").mkdir()
|
||||
(ui_root / "login" / "index.html").write_text("login")
|
||||
(ui_root / "_next").mkdir()
|
||||
(ui_root / "_next" / "skip.html").write_text("asset")
|
||||
|
||||
assert proxy_server._is_ui_pre_restructured(str(ui_root)) is True
|
||||
|
||||
|
||||
def test_is_ui_pre_restructured_unrestructured(tmp_path):
|
||||
from litellm.proxy import proxy_server
|
||||
|
||||
ui_root = tmp_path / "ui"
|
||||
ui_root.mkdir()
|
||||
(ui_root / "index.html").write_text("root")
|
||||
(ui_root / "login.html").write_text("login")
|
||||
|
||||
assert proxy_server._is_ui_pre_restructured(str(ui_root)) is False
|
||||
|
||||
|
||||
def test_is_ui_pre_restructured_mixed_state_returns_false(tmp_path):
|
||||
"""
|
||||
Regression: a fresh `<route>.html` checked out next to a stale
|
||||
`<route>/index.html` from a previous restructure run must return False,
|
||||
otherwise the stale subdirectory is served and points at chunk hashes
|
||||
that no longer exist in the new build.
|
||||
"""
|
||||
from litellm.proxy import proxy_server
|
||||
|
||||
ui_root = tmp_path / "ui"
|
||||
ui_root.mkdir()
|
||||
(ui_root / "index.html").write_text("root")
|
||||
(ui_root / "login.html").write_text("fresh login")
|
||||
(ui_root / "login").mkdir()
|
||||
(ui_root / "login" / "index.html").write_text("stale login")
|
||||
|
||||
assert proxy_server._is_ui_pre_restructured(str(ui_root)) is False
|
||||
|
||||
|
||||
def test_restructure_always_happens(monkeypatch):
|
||||
"""
|
||||
Test that restructuring logic always executes regardless of LITELLM_NON_ROOT setting.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue