fix(vector_stores): return 400 when a MongoDB TLS file cannot be read

tlsCAFile and tlsCertificateKeyFile are how a self-managed deployment presents a
private CA, so they are the options on-prem operators actually set. pymongo opens
those files itself during TLS setup and lets OSError out, which is neither a
PyMongoError nor a ValueError, so it missed every branch of the translator and
litellm.exception_type turned it into a 500 with a traceback in the body. A
mistyped path, or one that exists on the host but not inside the container, is a
routine mistake and has to read as a 400 naming the file.

Matched on the exception carrying a filename so a socket-level OSError still falls
through to the branches that handle it. Verified against a self-managed mongod with
a missing CA file, a CA path that is a directory, and a missing client certificate.
This commit is contained in:
Yuneng Jiang 2026-09-04 13:59:07 -07:00
parent 50fb35e17e
commit 323f51269d
No known key found for this signature in database
2 changed files with 51 additions and 0 deletions

View file

@ -267,6 +267,14 @@ def translate_mongo_error(error: Exception, index_name: str, database: str, coll
)
if isinstance(error, InvalidOperation):
return config_error(f"The MongoDB client was already closed or is unusable. Driver detail: {error}")
# A tlsCAFile or tlsCertificateKeyFile the process cannot open raises OSError from the TLS setup
# rather than a PyMongoError, and those options are how self-managed deployments present a private CA
if isinstance(error, OSError) and error.filename:
return config_error(
f"'{error.filename}', named by a TLS option in mongodb_connection_string, could not be read. "
"Check that tlsCAFile and tlsCertificateKeyFile point at files this process can open; inside "
f"a container that is the path in the container, not on the host. Driver detail: {error}"
)
# pymongo raises a plain ValueError, not a PyMongoError, for an unusable port, which an unescaped
# ':' in a password also produces, and which would otherwise reach the caller as a 500
if isinstance(error, ValueError):

View file

@ -1362,3 +1362,46 @@ class TestUnescapedCredentialsAreDiagnosed:
assert isinstance(translated, BadRequestError)
assert "database name in the URI path" in str(translated)
class TestUnreadableTlsFilesAreDiagnosed:
"""A private CA is how self-managed deployments present TLS, so tlsCAFile and
tlsCertificateKeyFile are on-prem options in practice. pymongo opens those files itself and
lets OSError out, which is not a PyMongoError, so before this they reached the caller as a 500
with a traceback. The errors here come from pymongo's real TLS setup."""
@staticmethod
def _real_tls_error(uri):
from pymongo import MongoClient
try:
MongoClient(uri, serverSelectionTimeoutMS=1500).admin.command("ping")
except Exception as e:
return e
raise AssertionError(f"expected {uri!r} to fail")
def _translated(self, uri):
return translate_mongo_error(self._real_tls_error(uri), index_name=INDEX, database="db", collection="c")
@pytest.mark.parametrize(
"path",
["/nonexistent-directory-for-tests/ca.pem", "/tmp"],
)
def test_an_unreadable_ca_file_is_a_400_naming_the_path(self, path):
translated = self._translated(f"mongodb://localhost:27717/?tls=true&tlsCAFile={path}")
assert isinstance(translated, BadRequestError)
assert path in str(translated)
assert "tlsCAFile" in str(translated)
def test_an_unreadable_client_certificate_is_a_400_naming_the_path(self):
path = "/nonexistent-directory-for-tests/client.pem"
translated = self._translated(f"mongodb://localhost:27717/?tls=true&tlsCertificateKeyFile={path}")
assert isinstance(translated, BadRequestError)
assert path in str(translated)
def test_an_oserror_carrying_no_filename_is_left_for_the_other_branches(self):
translated = translate_mongo_error(OSError("socket hung up"), index_name=INDEX, database="db", collection="c")
assert not isinstance(translated, BadRequestError)