fix(vector_stores): translate MongoDB client construction failures too

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. It
sat outside the try that translates driver errors, so a malformed URI or an
unresolvable cluster escaped as a raw pymongo exception and reached the caller as
a 500 with a traceback in the body.

The three DNS-shaped failures are also told apart now: a lookup that ran out of
time is a Timeout, a cluster name that is not in DNS says so and points at the
URI Atlas shows under Connect Drivers, and anything else keeps the generic
"not a usable MongoDB connection string".

Verified live: a tampered scheme, a nonexistent cluster and a 1ms timeout each
come back as their own message instead of a traceback.
This commit is contained in:
Yuneng Jiang 2026-09-02 11:28:33 -07:00
parent 1fed1029e0
commit 9434e563f3
No known key found for this signature in database
3 changed files with 76 additions and 4 deletions

View file

@ -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}"

View file

@ -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:

View file

@ -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)