diff --git a/litellm/llms/mongodb/common_utils.py b/litellm/llms/mongodb/common_utils.py index c2d081b8d8d..bf3bf953772 100644 --- a/litellm/llms/mongodb/common_utils.py +++ b/litellm/llms/mongodb/common_utils.py @@ -181,6 +181,7 @@ def translate_mongo_error(error: Exception, index_name: str, database: str, coll try: from pymongo.errors import ( ConfigurationError, + ConnectionFailure, ExecutionTimeout, InvalidOperation, NetworkTimeout, @@ -202,6 +203,14 @@ def translate_mongo_error(error: Exception, index_name: str, database: str, coll f"The MongoDB vector search against '{database}.{collection}' timed out before returning. " f"Driver detail: {error}" ) + # ServerSelectionTimeoutError and NetworkTimeout both sit under ConnectionFailure, so this + # only sees what those two branches left: a dropped or refused connection + if isinstance(error, ConnectionFailure): + return config_error( + f"The connection to '{database}.{collection}' was refused or dropped. On Atlas this is " + "usually a connection string with no username and password, or a TLS failure. Confirm " + f"the URI is the one Atlas shows under Connect, Drivers. Driver detail: {error}" + ) if isinstance(error, OperationFailure): code: Final = error.code detail: Final = str(error).lower() @@ -247,4 +256,11 @@ 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}") + # pymongo's URI parser raises a plain ValueError, not a PyMongoError, for a password holding an + # unescaped '/', which would otherwise reach the caller as a 500 + if isinstance(error, ValueError): + return config_error( + "mongodb_connection_string could not be parsed. A username or password containing " + f"'@', '/', ':' or '%' has to be percent-encoded per RFC 3986. Driver detail: {error}" + ) return error diff --git a/tests/test_litellm/llms/mongodb/vector_stores/test_mongodb_transformation.py b/tests/test_litellm/llms/mongodb/vector_stores/test_mongodb_transformation.py index 668fa676692..d60504c31e5 100644 --- a/tests/test_litellm/llms/mongodb/vector_stores/test_mongodb_transformation.py +++ b/tests/test_litellm/llms/mongodb/vector_stores/test_mongodb_transformation.py @@ -759,6 +759,42 @@ class TestErrorTranslation: assert "rejected the credentials" in str(translated) + def test_a_dropped_connection_is_a_400_not_an_unhandled_driver_error(self): + """AutoReconnect sits under ConnectionFailure alongside the two timeout classes, and Atlas + answers a URI with no credentials by closing the connection rather than failing auth. Left + untranslated it is not a litellm exception type, so it reaches the caller as a 500.""" + from pymongo.errors import AutoReconnect + + translated = self._translate(AutoReconnect("connection closed")) + + assert isinstance(translated, BadRequestError) + assert "refused or dropped" in str(translated) + assert "no username and password" in str(translated) + + def test_server_selection_timeout_still_wins_over_the_connection_branch(self): + from pymongo.errors import ServerSelectionTimeoutError + + translated = self._translate(ServerSelectionTimeoutError("no servers")) + + assert isinstance(translated, Timeout) + assert "refused or dropped" not in str(translated) + + def test_network_timeout_still_wins_over_the_connection_branch(self): + from pymongo.errors import NetworkTimeout + + translated = self._translate(NetworkTimeout("socket timed out")) + + assert isinstance(translated, Timeout) + assert "refused or dropped" not in str(translated) + + def test_an_unescaped_password_character_is_a_400_not_a_500(self): + """pymongo's URI parser raises a plain ValueError, not a PyMongoError, when a password + holds an unescaped '/'. That is a routine mistake and it must not be a 500.""" + translated = self._translate(ValueError("Port contains non-digit characters")) + + assert isinstance(translated, BadRequestError) + assert "percent-encoded" in str(translated) + def test_unauthorized_points_at_the_database_user_permissions(self): from pymongo.errors import OperationFailure