fix(qdrant): use get_collection for startup probe (multi-tenant safe, take 2)
Follow-up to PR #778 — `collection_exists()` is also denied by Qdrant Cloud on a collection-scoped JWT, so the multi-tenant fix needs to go one step further: use `get_collection(name)` (the underlying GET `/collections/{name}` call) and treat a 404 `UnexpectedResponse` as the "doesn't exist" signal. That endpoint is the only existence-probe Qdrant permits on a collection-scoped JWT — listing or probing collection metadata cluster-wide is a tenant-isolation boundary by design. Hit during Astrolabe Cloud smoke17 with the post-#778 image: qdrant_client.http.exceptions.UnexpectedResponse: 403 (Forbidden) raw response: {"error":"forbidden"} File "qdrant_client.py", line 84, in get_qdrant_client collection_present = await _qdrant_client.collection_exists(...) Folds the existence check into the same `get_collection()` call that already runs immediately afterward for dimension validation, so the new path is also one fewer round-trip on the happy path. Cold-start (collection genuinely missing) behavior is unchanged: 404 → `collection_info` is None → fall through to `create_collection()`. Whether `create_collection` succeeds is an orthogonal concern (managed multi-tenant setups pre-provision collections externally; admin-key single-tenant setups can create on the fly). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
2ac0a926c0
commit
04bc2325f2
@@ -3,6 +3,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from qdrant_client import AsyncQdrantClient, models
|
from qdrant_client import AsyncQdrantClient, models
|
||||||
|
from qdrant_client.http.exceptions import UnexpectedResponse
|
||||||
from qdrant_client.models import Distance, VectorParams
|
from qdrant_client.models import Distance, VectorParams
|
||||||
|
|
||||||
from nextcloud_mcp_server.config import get_settings
|
from nextcloud_mcp_server.config import get_settings
|
||||||
@@ -71,26 +72,33 @@ async def get_qdrant_client() -> AsyncQdrantClient:
|
|||||||
|
|
||||||
expected_dimension = embedding_service.get_dimension()
|
expected_dimension = embedding_service.get_dimension()
|
||||||
|
|
||||||
# Explicitly check if collection exists.
|
# Existence check folded into the get_collection() call.
|
||||||
#
|
#
|
||||||
# Use `collection_exists(name)` (per-collection HEAD-style probe)
|
# In managed multi-tenant Qdrant Cloud setups, per-tenant JWTs are
|
||||||
# rather than `get_collections()` (cluster-wide list). In managed
|
# scoped to a single collection (`access: [{"collection": "...",
|
||||||
# multi-tenant Qdrant Cloud setups, per-tenant JWTs are scoped
|
# "access": "rw"}]`) and Qdrant denies the cluster-level meta
|
||||||
# to a single collection and `get_collections()` returns 403
|
# endpoints `GET /collections` (used by `get_collections()`) and
|
||||||
# Forbidden by design — listing other tenants' collections would
|
# `GET /collections/{name}/exists` (used by `collection_exists()`)
|
||||||
# be a security regression. `collection_exists` only requires
|
# with 403 Forbidden — by design, since listing or probing
|
||||||
# access to the named collection, which the tenant JWT has.
|
# collections cluster-wide is a tenant-isolation boundary.
|
||||||
logger.debug(f"Checking if collection '{collection_name}' exists...")
|
# `GET /collections/{name}` (the underlying call for
|
||||||
collection_present = await _qdrant_client.collection_exists(
|
# `get_collection()`) is the only existence-probe permitted on a
|
||||||
collection_name=collection_name
|
# collection-scoped JWT — it returns 200 with the collection
|
||||||
)
|
# detail on hit and 404 on miss.
|
||||||
|
logger.debug(f"Fetching collection '{collection_name}' details...")
|
||||||
|
collection_info = None
|
||||||
|
try:
|
||||||
|
collection_info = await _qdrant_client.get_collection(collection_name)
|
||||||
|
except UnexpectedResponse as exc:
|
||||||
|
if exc.status_code != 404:
|
||||||
|
raise
|
||||||
|
logger.debug(f"Collection '{collection_name}' not found (404).")
|
||||||
|
|
||||||
if collection_present:
|
if collection_info is not None:
|
||||||
# Collection exists - validate dimensions
|
# Collection exists - validate dimensions
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Collection '{collection_name}' found, validating dimensions..."
|
f"Collection '{collection_name}' found, validating dimensions..."
|
||||||
)
|
)
|
||||||
collection_info = await _qdrant_client.get_collection(collection_name)
|
|
||||||
# Handle both named vectors (dict) and legacy single vector
|
# Handle both named vectors (dict) and legacy single vector
|
||||||
vectors = collection_info.config.params.vectors
|
vectors = collection_info.config.params.vectors
|
||||||
if isinstance(vectors, dict):
|
if isinstance(vectors, dict):
|
||||||
|
|||||||
Reference in New Issue
Block a user