From b920be3ee73146bc29fd64eb9839b60f8b1a948f Mon Sep 17 00:00:00 2001 From: Harshit Jain <48647625+Harshit28j@users.noreply.github.com> Date: Mon, 26 Jan 2026 23:18:06 +0530 Subject: [PATCH] fix: server rooth path (#19790) --- litellm/proxy/proxy_server.py | 1 + litellm/proxy/utils.py | 26 +++++--- .../proxy_unit_tests/test_server_root_path.py | 64 +++++++++++++++++++ 3 files changed, 82 insertions(+), 9 deletions(-) create mode 100644 tests/proxy_unit_tests/test_server_root_path.py diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 0aa8ccff1d..16cdd9da64 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -825,6 +825,7 @@ app = FastAPI( title=_title, description=_description, version=version, + root_path=server_root_path, lifespan=proxy_startup_event, # type: ignore[reportGeneralTypeIssues] ) diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index 83296927a7..13f42a2f71 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -982,7 +982,9 @@ class ProxyLogging: try: # Check if load balancing should be used - if guardrail_name and self._should_use_guardrail_load_balancing(guardrail_name): + if guardrail_name and self._should_use_guardrail_load_balancing( + guardrail_name + ): response = await self._execute_guardrail_with_load_balancing( guardrail_name=guardrail_name, hook_type="pre_call", @@ -1017,7 +1019,11 @@ class ProxyLogging: latency_seconds = guardrail_end_time - guardrail_start_time # Get guardrail name for metrics (fallback if not set) - metrics_guardrail_name = guardrail_name or getattr(callback, "guardrail_name", callback.__class__.__name__) or "unknown" + metrics_guardrail_name = ( + guardrail_name + or getattr(callback, "guardrail_name", callback.__class__.__name__) + or "unknown" + ) # Find PrometheusLogger in callbacks and record metrics for prom_callback in litellm.callbacks: @@ -2218,17 +2224,17 @@ class PrismaClient: ) -> Optional[dict]: """ Execute a query with automatic fallback for PostgreSQL cached plan errors. - + This handles the "cached plan must not change result type" error that occurs during rolling deployments when schema changes are applied while old pods still have cached query plans expecting the old schema. - + Args: sql_query: SQL query string to execute - + Returns: Query result or None - + Raises: Original exception if not a cached plan error """ @@ -2241,7 +2247,7 @@ class PrismaClient: # Add a unique comment to make the query different sql_query_retry = sql_query.replace( "SELECT", - f"SELECT /* cache_invalidated_{int(time.time() * 1000)} */" + f"SELECT /* cache_invalidated_{int(time.time() * 1000)} */", ) verbose_proxy_logger.warning( "PostgreSQL cached plan error detected for token lookup, " @@ -2583,7 +2589,9 @@ class PrismaClient: WHERE v.token = '{token}' """ - response = await self._query_first_with_cached_plan_fallback(sql_query) + response = await self._query_first_with_cached_plan_fallback( + sql_query + ) if response is not None: if response["team_models"] is None: @@ -4227,7 +4235,7 @@ def get_server_root_path() -> str: - If SERVER_ROOT_PATH is set, return it. - Otherwise, default to "/". """ - return os.getenv("SERVER_ROOT_PATH", "/") + return os.getenv("SERVER_ROOT_PATH", "") def get_prisma_client_or_throw(message: str): diff --git a/tests/proxy_unit_tests/test_server_root_path.py b/tests/proxy_unit_tests/test_server_root_path.py new file mode 100644 index 0000000000..4b39558e15 --- /dev/null +++ b/tests/proxy_unit_tests/test_server_root_path.py @@ -0,0 +1,64 @@ +import os +from unittest import mock +from litellm.proxy import utils + + +# Test the utility function logic +def test_get_server_root_path_unset(): + """ + Test that get_server_root_path returns empty string when SERVER_ROOT_PATH is unset + """ + with mock.patch.dict(os.environ, {}, clear=True): + # We need to make sure SERVER_ROOT_PATH is not in env + if "SERVER_ROOT_PATH" in os.environ: + del os.environ["SERVER_ROOT_PATH"] + + root_path = utils.get_server_root_path() + assert ( + root_path == "" + ), "Should return empty string when unset to allow X-Forwarded-Prefix" + + +def test_get_server_root_path_set(): + """ + Test that get_server_root_path returns the value when SERVER_ROOT_PATH is set + """ + with mock.patch.dict(os.environ, {"SERVER_ROOT_PATH": "/my-path"}, clear=True): + root_path = utils.get_server_root_path() + assert root_path == "/my-path", "Should return the set value" + + +def test_get_server_root_path_empty_string(): + """ + Test that get_server_root_path returns empty string when SERVER_ROOT_PATH is explicitly empty + """ + with mock.patch.dict(os.environ, {"SERVER_ROOT_PATH": ""}, clear=True): + root_path = utils.get_server_root_path() + assert ( + root_path == "" + ), "Should return empty string when explicitly set to empty" + + +# Integration test simulation for FastAPI app initialization +def test_fastapi_app_initialization_mock(): + """ + Simulate how proxy_server.py initializes FastAPI app with the root_path. + We don't import proxy_server because it has global side effects/singletons. + Instead we verify the logic flow. + """ + from fastapi import FastAPI + + # CASE 1: Proxy Mode (Unset) + with mock.patch.dict(os.environ, {}, clear=True): + if "SERVER_ROOT_PATH" in os.environ: + del os.environ["SERVER_ROOT_PATH"] + + server_root_path = utils.get_server_root_path() + app = FastAPI(root_path=server_root_path) + assert app.root_path == "" + + # CASE 2: Direct Mode (Set) + with mock.patch.dict(os.environ, {"SERVER_ROOT_PATH": "/custom-root"}, clear=True): + server_root_path = utils.get_server_root_path() + app = FastAPI(root_path=server_root_path) + assert app.root_path == "/custom-root"