This commit is contained in:
SYED ALI ABBAS RAHIL 2026-09-28 12:52:01 -07:00 • committed by GitHub
commit 852016b7e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 78 additions and 4 deletions

View file

@ -127,6 +127,7 @@ from litellm.proxy.management_endpoints.types import (
from litellm.proxy.utils import (
PrismaClient,
ProxyLogging,
get_cookie_path_from_server_root_path,
get_custom_url,
get_server_root_path,
)
@ -2821,6 +2822,7 @@ def set_session_token_cookie(response: Response, request: Request, jwt_token: st
response.set_cookie(
key="token",
value=jwt_token,
path=get_cookie_path_from_server_root_path(),
secure=IPAddressUtils.is_request_https(request),
httponly=False,
samesite="lax",

View file

@ -8314,6 +8314,20 @@ def get_server_root_path() -> str:
return os.getenv("SERVER_ROOT_PATH", "")
def get_cookie_path_from_server_root_path() -> str:
"""
Cookie `path` scoped to SERVER_ROOT_PATH.
Ensures auth cookies for deployments served under different root paths
(e.g. `a.com` vs `a.com/prefix`) do not overwrite each other. Defaults to
"/" when SERVER_ROOT_PATH is unset.
"""
root_path = get_server_root_path()
if not root_path or root_path == "/":
return "/"
return "/" + root_path.strip("/")
def normalize_route_for_root_path(route: str) -> str | None:
"""Strip SERVER_ROOT_PATH prefix. Returns de-prefixed route, or None if route is not under root path."""
root_path: Final = get_server_root_path()

View file

@ -17,7 +17,7 @@ from litellm.types.guardrails import GuardrailEventHooks
from unittest.mock import AsyncMock, MagicMock, patch
from litellm.proxy.utils import get_custom_url, join_paths
from litellm.proxy.utils import get_cookie_path_from_server_root_path, get_custom_url, join_paths
def test_get_custom_url(monkeypatch):
@ -26,6 +26,26 @@ def test_get_custom_url(monkeypatch):
assert custom_url == "http://0.0.0.0:4000/litellm/ui/"
@pytest.mark.parametrize(
"server_root_path, expected",
[
(None, "/"),
("", "/"),
("/", "/"),
("/litellm", "/litellm"),
("litellm", "/litellm"),
("/litellm/", "/litellm"),
("/team/a", "/team/a"),
],
)
def test_get_cookie_path_from_server_root_path(monkeypatch, server_root_path, expected):
if server_root_path is None:
monkeypatch.delenv("SERVER_ROOT_PATH", raising=False)
else:
monkeypatch.setenv("SERVER_ROOT_PATH", server_root_path)
assert get_cookie_path_from_server_root_path() == expected
def test_proxy_only_error_true_for_llm_route():
proxy_logging_obj = ProxyLogging(user_api_key_cache=DualCache())
assert proxy_logging_obj._is_proxy_only_llm_api_error(

View file

@ -128,6 +128,34 @@ describe("cookieUtils", () => {
vi.restoreAllMocks();
});
it("should clear token cookie at the server root path when deployed under SERVER_ROOT_PATH", () => {
const originalLocation = window.location;
vi.stubGlobal("location", { ...originalLocation, pathname: "/litellm/ui/" });
const cookieSpy = vi.spyOn(document, "cookie", "set");
clearTokenCookies();
expect(cookieSpy).toHaveBeenCalledWith(expect.stringContaining("path=/litellm;"));
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it("should clear token cookie at the server root path when the root path contains a /ui segment", () => {
const originalLocation = window.location;
vi.stubGlobal("location", { ...originalLocation, pathname: "/foo/ui/bar/ui/" });
const cookieSpy = vi.spyOn(document, "cookie", "set");
clearTokenCookies();
expect(cookieSpy).toHaveBeenCalledWith(expect.stringContaining("path=/foo/ui/bar;"));
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it("should clear sessionStorage token", () => {
sessionStorage.setItem("token", "stored-token");
clearTokenCookies();

View file

@ -14,9 +14,12 @@ function getUiCookiePath(): string {
if (typeof window === "undefined") return "/ui";
// Match "/ui" only as a full path segment (followed by "/" or end of string)
// to avoid false matches like "/my-ui-tool/login" → "/my-ui".
const match = window.location.pathname.match(/\/ui(?=\/|$)/);
if (match && match.index !== undefined) {
return window.location.pathname.substring(0, match.index + 3);
// The UI mounts at the last "/ui" segment (SERVER_ROOT_PATH + "/ui"), so use the
// last match to stay correct when the root path itself contains a "/ui" segment.
const matches = [...window.location.pathname.matchAll(/\/ui(?=\/|$)/g)];
const lastMatch = matches[matches.length - 1];
if (lastMatch && lastMatch.index !== undefined) {
return window.location.pathname.substring(0, lastMatch.index + 3);
}
return "/ui";
}
@ -38,6 +41,13 @@ export function clearTokenCookies() {
const uiCookiePath = getUiCookiePath();
const paths = ["/", uiCookiePath];
// Clear at the server root path (e.g. "/litellm") too, since the server-set
// auth cookie is scoped there when SERVER_ROOT_PATH is configured.
const serverRootPath = uiCookiePath.replace(/\/ui$/, "");
if (serverRootPath && !paths.includes(serverRootPath)) {
paths.push(serverRootPath);
}
// Add the current path directory if it's different from root and /ui
if (currentPath && currentPath !== "/" && !currentPath.startsWith("/ui")) {
const dirPath = currentPath.substring(0, currentPath.lastIndexOf("/") + 1);