mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-12 23:01:41 +00:00
fix: rewrite absolute /ui/assets/ paths when SERVER_ROOT_PATH is set
Some JS chunks in the packaged Next.js UI output use absolute paths like
"/ui/assets/logos/" instead of the litellm-asset-prefix token. When the
proxy is served under a sub-path (SERVER_ROOT_PATH=/web-llmgateway/v1),
those absolute paths are not rewritten, so browsers request
/{SERVER_ROOT_PATH}/assets/logos/... (without /ui) and receive 404s.
Add a targeted string replacement alongside the existing litellm-asset-prefix
substitution so that '"/ui/assets/' becomes '"{server_root_path}/ui/assets/'
in all text files under the UI directory.
Fixes https://github.com/BerriAI/litellm/issues/25283
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
62757ff48f
commit
921b06f7cd
2 changed files with 128 additions and 0 deletions
|
|
@ -1373,6 +1373,15 @@ try:
|
|||
f"{server_root_path}",
|
||||
)
|
||||
|
||||
# Replace absolute /ui/assets/ paths with server_root_path-prefixed
|
||||
# equivalents so logo/asset requests include the root path prefix.
|
||||
# Some JS bundles emit absolute paths like "/ui/assets/logos/" which
|
||||
# resolve incorrectly when the app is served under a sub-path.
|
||||
modified_content = modified_content.replace(
|
||||
'"/ui/assets/',
|
||||
f'"{server_root_path}/ui/assets/',
|
||||
)
|
||||
|
||||
# Replace the /.well-known/litellm-ui-config with the server root path
|
||||
modified_content = modified_content.replace(
|
||||
"/litellm/.well-known/litellm-ui-config",
|
||||
|
|
|
|||
119
tests/test_litellm/proxy/test_server_root_path_ui_assets.py
Normal file
119
tests/test_litellm/proxy/test_server_root_path_ui_assets.py
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
"""
|
||||
Tests for SERVER_ROOT_PATH rewriting of absolute /ui/assets/ paths in JS bundles.
|
||||
|
||||
Regression test for https://github.com/BerriAI/litellm/issues/25283
|
||||
|
||||
When LiteLLM is served under a sub-path (SERVER_ROOT_PATH=/web-llmgateway/v1),
|
||||
some JS chunks emit absolute paths like "/ui/assets/logos/" that the browser
|
||||
resolves without the sub-path prefix, causing 404s for logo assets.
|
||||
The startup path-rewriting logic must patch these absolute paths.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def _apply_server_root_path_replacements(
|
||||
ui_path: str,
|
||||
server_root_path: str,
|
||||
litellm_asset_prefix: str = "/litellm-asset-prefix",
|
||||
) -> None:
|
||||
"""Replicate the replacement logic from proxy_server.py for testing."""
|
||||
skip_extensions = (".png", ".jpg", ".jpeg", ".gif", ".ico", ".woff", ".woff2", ".ttf", ".eot")
|
||||
for root, _, files in os.walk(ui_path):
|
||||
for filename in files:
|
||||
if filename.endswith(skip_extensions):
|
||||
continue
|
||||
file_path = os.path.join(root, filename)
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
modified = content.replace(litellm_asset_prefix, server_root_path)
|
||||
modified = modified.replace('"/ui/assets/', f'"{server_root_path}/ui/assets/')
|
||||
modified = modified.replace(
|
||||
"/litellm/.well-known/litellm-ui-config",
|
||||
f"{server_root_path}/.well-known/litellm-ui-config",
|
||||
)
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write(modified)
|
||||
except (UnicodeDecodeError, PermissionError, OSError):
|
||||
continue
|
||||
|
||||
|
||||
def test_absolute_ui_asset_paths_rewritten_with_server_root_path(tmp_path):
|
||||
"""
|
||||
JS bundles that use absolute "/ui/assets/logos/" paths should have those
|
||||
paths rewritten to include SERVER_ROOT_PATH when it is set.
|
||||
|
||||
Reproduces: browser requests /{SERVER_ROOT_PATH}/assets/logos/... (404)
|
||||
instead of /{SERVER_ROOT_PATH}/ui/assets/logos/... (200).
|
||||
"""
|
||||
ui_path = tmp_path / "ui"
|
||||
ui_path.mkdir()
|
||||
|
||||
# Simulate a JS chunk that uses an absolute /ui/assets/ path.
|
||||
# This mirrors the pattern seen in the packaged UI output:
|
||||
# _next/static/chunks/0d219667baa010f5.js
|
||||
js_file = ui_path / "_next" / "static" / "chunks" / "logos.js"
|
||||
js_file.parent.mkdir(parents=True)
|
||||
js_file.write_text('let eq="/ui/assets/logos/",r={github:`${eq}github.svg`}')
|
||||
|
||||
server_root_path = "/web-llmgateway/v1"
|
||||
_apply_server_root_path_replacements(str(ui_path), server_root_path)
|
||||
|
||||
result = js_file.read_text()
|
||||
assert f'"{server_root_path}/ui/assets/logos/"' in result, (
|
||||
f"Absolute /ui/assets/ path not rewritten to include SERVER_ROOT_PATH. Got: {result}"
|
||||
)
|
||||
assert '"/ui/assets/logos/"' not in result, (
|
||||
"Old absolute /ui/assets/ path should have been replaced"
|
||||
)
|
||||
|
||||
|
||||
def test_litellm_asset_prefix_still_rewritten(tmp_path):
|
||||
"""Existing litellm-asset-prefix replacement must still work alongside the new fix."""
|
||||
ui_path = tmp_path / "ui"
|
||||
ui_path.mkdir()
|
||||
|
||||
js_file = ui_path / "bundle.js"
|
||||
js_file.write_text(
|
||||
'let t="/litellm-asset-prefix/_next/",eq="/ui/assets/logos/"'
|
||||
)
|
||||
|
||||
server_root_path = "/myapp"
|
||||
_apply_server_root_path_replacements(str(ui_path), server_root_path)
|
||||
|
||||
result = js_file.read_text()
|
||||
assert f'"{server_root_path}/_next/"' in result
|
||||
assert f'"{server_root_path}/ui/assets/logos/"' in result
|
||||
assert '"/litellm-asset-prefix/_next/"' not in result
|
||||
assert '"/ui/assets/logos/"' not in result
|
||||
|
||||
|
||||
def test_no_replacement_when_no_server_root_path(tmp_path):
|
||||
"""When server_root_path is empty, paths should remain unchanged."""
|
||||
ui_path = tmp_path / "ui"
|
||||
ui_path.mkdir()
|
||||
|
||||
original = 'let eq="/ui/assets/logos/",r={}'
|
||||
js_file = ui_path / "bundle.js"
|
||||
js_file.write_text(original)
|
||||
|
||||
# Simulate the guard: only replace when server_root_path is non-empty
|
||||
server_root_path = ""
|
||||
if server_root_path and server_root_path != "/":
|
||||
_apply_server_root_path_replacements(str(ui_path), server_root_path)
|
||||
|
||||
assert js_file.read_text() == original
|
||||
|
||||
|
||||
def test_binary_files_are_skipped(tmp_path):
|
||||
"""Image and font files must not be touched by the replacement logic."""
|
||||
ui_path = tmp_path / "ui"
|
||||
ui_path.mkdir()
|
||||
|
||||
png_file = ui_path / "logo.png"
|
||||
png_file.write_bytes(b"\x89PNG\r\n\x1a\n")
|
||||
|
||||
_apply_server_root_path_replacements(str(ui_path), "/root")
|
||||
|
||||
assert png_file.read_bytes() == b"\x89PNG\r\n\x1a\n"
|
||||
Loading…
Add table
Reference in a new issue