[Fix] UI logo not showing when UI_LOGO_PATH is a local file path

- Fix /get_image to use mimetypes.guess_type() for correct Content-Type
  instead of hardcoding image/jpeg for all file types (breaks PNG logos
  in strict enterprise proxy environments)
- Fix get_ui_theme_settings to null out logo_url when it is a local
  filesystem path: the browser cannot load local paths as image URLs,
  so the UI must fall back to /get_image which serves local files via
  the UI_LOGO_PATH env var
- Add test covering the local-path nulling behavior

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
yuneng-jiang 2026-03-04 20:43:22 -08:00
parent 335c4d4946
commit 1f6f16d035
3 changed files with 37 additions and 3 deletions

View file

@ -3,6 +3,7 @@ import copy
import enum
import inspect
import io
import mimetypes
import os
import random
import secrets
@ -11061,10 +11062,16 @@ async def get_image():
logo_path = os.getenv("UI_LOGO_PATH", default_logo)
verbose_proxy_logger.debug("Reading logo from path: %s", logo_path)
def _get_image_media_type(path: str) -> str:
media_type, _ = mimetypes.guess_type(path)
if media_type and media_type.startswith("image/"):
return media_type
return "image/jpeg"
# If UI_LOGO_PATH points to a local file, serve it directly (skip cache)
if logo_path != default_logo and not logo_path.startswith(("http://", "https://")):
if os.path.exists(logo_path):
return FileResponse(logo_path, media_type="image/jpeg")
return FileResponse(logo_path, media_type=_get_image_media_type(logo_path))
# Custom path doesn't exist — fall back to default
verbose_proxy_logger.warning(
f"UI_LOGO_PATH '{logo_path}' does not exist, falling back to default logo"
@ -11103,7 +11110,7 @@ async def get_image():
return FileResponse(default_logo, media_type="image/jpeg")
else:
# Return the local image file if the logo path is not an HTTP/HTTPS URL
return FileResponse(logo_path, media_type="image/jpeg")
return FileResponse(logo_path, media_type=_get_image_media_type(logo_path))
@app.get("/get_favicon", include_in_schema=False)

View file

@ -734,17 +734,28 @@ async def get_ui_theme_settings():
Note: This endpoint is public (no authentication required) so all users can see custom branding.
Only the /update/ui_theme_settings endpoint requires authentication for admins to change settings.
"""
import os
from litellm.proxy.proxy_server import proxy_config
# Load existing config
config = await proxy_config.get_config()
return await _get_settings_with_schema(
result = await _get_settings_with_schema(
settings_key="ui_theme_config",
settings_class=UIThemeConfig,
config=config,
)
# If logo_url is a local filesystem path (not an HTTP/HTTPS URL), the browser
# cannot load it directly as an image src. Null it out so the UI falls back to
# calling the /get_image endpoint, which reads UI_LOGO_PATH and serves the file.
logo_url = result["values"].get("logo_url")
if logo_url and not logo_url.startswith(("http://", "https://")):
result["values"]["logo_url"] = None
return result
@router.patch(
"/update/ui_theme_settings",

View file

@ -776,6 +776,22 @@ class TestProxySettingEndpoints:
== "https://example.com/favicon.ico"
)
def test_get_ui_theme_settings_local_path_is_nulled(self, mock_proxy_config):
"""
Local file paths stored in logo_url cannot be loaded by the browser as an image URL.
The endpoint should null them out so the UI falls back to /get_image, which reads
UI_LOGO_PATH from the environment and serves the file directly.
"""
mock_proxy_config["config"]["litellm_settings"]["ui_theme_config"] = {
"logo_url": "/app/company_logo.png",
}
response = client.get("/get/ui_theme_settings")
assert response.status_code == 200
data = response.json()
assert data["values"]["logo_url"] is None
def test_get_ui_settings(self, mock_auth, monkeypatch):
"""Test retrieving UI settings with allowlist sanitization"""
from unittest.mock import AsyncMock, MagicMock