diff --git a/litellm/llms/mongodb/common_utils.py b/litellm/llms/mongodb/common_utils.py index 4aafaf86a5e..48496eee170 100644 --- a/litellm/llms/mongodb/common_utils.py +++ b/litellm/llms/mongodb/common_utils.py @@ -121,6 +121,8 @@ _UNAUTHORIZED_CODE: Final = 13 # Atlas reports a rejected user as code 8000 "AtlasError" rather than 18, so the # message is the only reliable signal for a serverless or shared-tier deployment. _AUTHENTICATION_MESSAGE_MARKERS: Final = ("bad auth", "authentication failed", "not authorized") +_RESOLUTION_TIMEOUT_MARKERS: Final = ("resolution lifetime expired", "dns operation timed out") +_UNKNOWN_HOSTNAME_MARKERS: Final = ("dns query name does not exist", "name or service not known") def _index_hint(index_name: str, database: str, collection: str) -> str: @@ -206,6 +208,18 @@ def translate_mongo_error(error: Exception, index_name: str, database: str, coll f"'{index_name}'. Driver detail: {error}" ) if isinstance(error, ConfigurationError): + configuration_detail: Final = str(error).lower() + if any(marker in configuration_detail for marker in _RESOLUTION_TIMEOUT_MARKERS): + return timeout_error( + "The DNS lookup for the cluster in mongodb_connection_string did not finish in time. " + "A mongodb+srv:// URI needs an SRV lookup before any connection is attempted, so this " + f"is DNS or the configured timeout, not MongoDB. Driver detail: {error}" + ) + if any(marker in configuration_detail for marker in _UNKNOWN_HOSTNAME_MARKERS): + return config_error( + "The cluster hostname in mongodb_connection_string does not exist in DNS. Check the " + f"cluster name against the URI Atlas shows under Connect, Drivers. Driver detail: {error}" + ) return config_error( "mongodb_connection_string is not a usable MongoDB connection string. " f"Driver detail: {error}" diff --git a/litellm/llms/mongodb/vector_stores/transformation.py b/litellm/llms/mongodb/vector_stores/transformation.py index 8b8ca5188cf..4b792accc9f 100644 --- a/litellm/llms/mongodb/vector_stores/transformation.py +++ b/litellm/llms/mongodb/vector_stores/transformation.py @@ -338,9 +338,9 @@ class MongoDBVectorStoreConfig(BaseDirectVectorStoreConfig): vector_store_id, self._embedding_vector(embedding_response), params, vector_store_search_optional_params ) - client: Final = self.sync_client_factory(key) - target: Final = client[database][collection] # pyright: ignore[reportIndexIssue] # factory is typed as returning object so injected doubles are accepted try: + client: Final = self.sync_client_factory(key) + target: Final = client[database][collection] # pyright: ignore[reportIndexIssue] # factory is typed as returning object so injected doubles are accepted documents: Final = list(target.aggregate(pipeline)) except Exception as e: raise translate_mongo_error( @@ -382,9 +382,9 @@ class MongoDBVectorStoreConfig(BaseDirectVectorStoreConfig): vector_store_id, self._embedding_vector(embedding_response), params, vector_store_search_optional_params ) - client: Final = self.async_client_factory(key) - target: Final = client[database][collection] # pyright: ignore[reportIndexIssue] # factory is typed as returning object so injected doubles are accepted try: + client: Final = self.async_client_factory(key) + target: Final = client[database][collection] # pyright: ignore[reportIndexIssue] # factory is typed as returning object so injected doubles are accepted cursor: Final = await target.aggregate(pipeline) documents: Final = [document async for document in cursor] except Exception as e: 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 68437487176..84de42d2126 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 @@ -983,3 +983,61 @@ class TestUnrecognisedParameters: with pytest.raises(BadRequestError, match="mongodb_collectoin"): await _asearch(config, litellm_params={"mongodb_collectoin": "embedded_movies"}) + + +class TestClientConstructionFailures: + """Building the client parses the URI and, for mongodb+srv://, performs a DNS SRV lookup, so it + fails on exactly the inputs a user is most likely to get wrong. Constructing it outside the + translation boundary let those escape as raw pymongo errors, which litellm.exception_type then + wrapped into a 500 with a traceback in the body.""" + + def _config_that_fails_to_connect(self, error): + def factory(_key): + raise error + + return MongoDBVectorStoreConfig( + embedding_fn=FakeEmbeddingFn([0.1, 0.2, 0.3]), sync_client_factory=factory + ) + + def _async_config_that_fails_to_connect(self, error): + def factory(_key): + raise error + + return MongoDBVectorStoreConfig( + aembedding_fn=FakeAsyncEmbeddingFn([0.1, 0.2, 0.3]), async_client_factory=factory + ) + + def test_a_malformed_uri_is_a_bad_request_not_a_500(self): + from pymongo.errors import InvalidURI + + config = self._config_that_fails_to_connect(InvalidURI("Invalid URI scheme")) + + with pytest.raises(BadRequestError, match="not a usable MongoDB connection string"): + _search(config) + + def test_an_unresolvable_cluster_name_says_so(self): + from pymongo.errors import ConfigurationError + + config = self._config_that_fails_to_connect(ConfigurationError("The DNS query name does not exist")) + + with pytest.raises(BadRequestError, match="does not exist in DNS"): + _search(config) + + def test_a_dns_lookup_that_ran_out_of_time_is_a_timeout(self): + from pymongo.errors import ConfigurationError + + config = self._config_that_fails_to_connect( + ConfigurationError("The resolution lifetime expired after 0.291 seconds") + ) + + with pytest.raises(Timeout, match="did not finish in time"): + _search(config) + + @pytest.mark.asyncio + async def test_the_async_path_translates_them_too(self): + from pymongo.errors import InvalidURI + + config = self._async_config_that_fails_to_connect(InvalidURI("Invalid URI scheme")) + + with pytest.raises(BadRequestError, match="not a usable MongoDB connection string"): + await _asearch(config)