mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-08 22:21:35 +00:00
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:
parent
1fed1029e0
commit
9434e563f3
3 changed files with 76 additions and 4 deletions
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue