From 0a33ea40c625a960df71f7c2a43c2f9709ce4412 Mon Sep 17 00:00:00 2001 From: Yucheng Zhu Date: Thu, 23 Jul 2026 23:59:36 -0700 Subject: [PATCH] fix(proxy/batches): fail closed when the managed file ownership lookup errors A lookup exception previously fell back to dispatching the original unified id with the ownership gate unexecuted; the managed-files deployment hook maps unified ids from cache without re-checking ownership, so a database outage let a caller dispatch another tenant's file. Raise a clear 503 instead and lock the behavior with a regression test. No-database and no-row cases still fall back unchanged --- litellm/proxy/batches_endpoints/endpoints.py | 17 +++++++++++------ .../proxy/batches_endpoints/test_endpoints.py | 17 +++++++++++------ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/litellm/proxy/batches_endpoints/endpoints.py b/litellm/proxy/batches_endpoints/endpoints.py index f6a48baad06..ed2913fdf14 100644 --- a/litellm/proxy/batches_endpoints/endpoints.py +++ b/litellm/proxy/batches_endpoints/endpoints.py @@ -51,11 +51,13 @@ async def _resolve_managed_input_file_storage_url( ) -> "str | None": """Resolve a managed (unified) input_file_id to its backend storage_url. - Returns None when the proxy has no database, the lookup fails, no managed - file row exists, or the row has no storage_url; callers fall back to - dispatching the original id so the managed-files deployment hook can still - map it via model_file_id_mapping. Raises a 404 when the caller does not - own the managed file. + Returns None when the proxy has no database, no managed file row exists, + or the row has no storage_url; callers fall back to dispatching the + original id so the managed-files deployment hook can still map it via + model_file_id_mapping. Raises a 404 when the caller does not own the + managed file and a 503 when the lookup fails, so an unverifiable id is + never dispatched (the deployment hook maps ids from cache without + re-checking ownership). """ from litellm.proxy.proxy_server import prisma_client @@ -65,7 +67,10 @@ async def _resolve_managed_input_file_storage_url( db_file = await ManagedFileRepository(prisma_client).table.find_first(where={"unified_file_id": input_file_id}) except Exception as e: verbose_proxy_logger.warning("create_batch: managed file lookup failed for %s: %s", input_file_id, e) - return None + raise HTTPException( + status_code=503, + detail={"error": "Unable to verify managed file access; please retry"}, + ) if db_file is None: return None if not can_access_resource( diff --git a/tests/test_litellm/proxy/batches_endpoints/test_endpoints.py b/tests/test_litellm/proxy/batches_endpoints/test_endpoints.py index 1298a7c0b62..2aa16d041d4 100644 --- a/tests/test_litellm/proxy/batches_endpoints/test_endpoints.py +++ b/tests/test_litellm/proxy/batches_endpoints/test_endpoints.py @@ -605,10 +605,12 @@ async def test_create__unified_file_id_other_tenant_gets_404(harness): @pytest.mark.asyncio -async def test_create__unified_file_id_db_error_falls_back_to_raw_id(harness): - """A database failure during resolution must not abort batch creation: - the original id is dispatched so the managed-files deployment hook can - still map it via model_file_id_mapping.""" +async def test_create__unified_file_id_db_error_fails_closed_503(harness): + """A lookup failure must fail closed: the ownership gate could not run, and + the managed-files deployment hook maps unified ids from cache without + re-checking ownership, so dispatching the unverified id would let a caller + use another tenant's file during a database outage. Expect a clear 503 and + no dispatch.""" set_body( harness, { @@ -629,9 +631,12 @@ async def test_create__unified_file_id_db_error_falls_back_to_raw_id(harness): patch.object(proxy_server, "prisma_client", MagicMock()), patch.object(endpoints, "ManagedFileRepository", fake_repo_cls), ): - await call_create(harness) + with pytest.raises(ProxyException) as exc: + await call_create(harness) - assert harness.router_kwargs()["input_file_id"] == "litellm_proxy_unified_id" + assert exc.value.code == "503" + harness.router_acreate.assert_not_called() + harness.litellm_acreate.assert_not_called() @pytest.mark.asyncio