mirror of
https://github.com/giancarloerra/socraticode.git
synced 2026-07-03 14:05:21 +02:00
fix(docker): include api-key header in external Qdrant readiness probe
External Qdrant deployments that require authentication (notably Qdrant Cloud, which returns 403 on every endpoint including /healthz without an api-key header) cannot pass the existing readiness check, so codebase_index fails with "Cannot reach external Qdrant" even though codebase_health and codebase_search still succeed -- their qdrant-js client paths attach the key automatically. Forward QDRANT_API_KEY as an `api-key` request header from ensureExternalQdrantReady() by extending waitForService() with an optional RequestInit. Docker-managed and unauthenticated external Qdrant deployments remain unaffected (no init forwarded -> no headers). Covered by two new unit tests in docker.test.ts.
This commit is contained in:
+17
-5
@@ -7,6 +7,7 @@ import {
|
||||
OLLAMA_HOST,
|
||||
OLLAMA_IMAGE,
|
||||
OLLAMA_PORT,
|
||||
QDRANT_API_KEY,
|
||||
QDRANT_CONTAINER_NAME,
|
||||
QDRANT_GRPC_PORT,
|
||||
QDRANT_HOST,
|
||||
@@ -203,13 +204,20 @@ async function ensureExternalQdrantReady(onProgress?: InfraProgressCallback): Pr
|
||||
onProgress?.(`Checking external Qdrant at ${baseUrl}...`);
|
||||
logger.info("Checking external Qdrant", { url: baseUrl });
|
||||
|
||||
// Qdrant Cloud requires authentication on every endpoint, including /healthz
|
||||
// (returns 403 without an api-key header). Locally run Qdrant typically does
|
||||
// not, so the header is sent only when QDRANT_API_KEY is configured.
|
||||
const init: RequestInit | undefined = QDRANT_API_KEY
|
||||
? { headers: { "api-key": QDRANT_API_KEY } }
|
||||
: undefined;
|
||||
|
||||
try {
|
||||
// 5 retries × 1 s — fast fail for misconfiguration, brief grace for transient flakiness
|
||||
await waitForService(healthUrl, "Qdrant", 5, 1000);
|
||||
await waitForService(healthUrl, "Qdrant", 5, 1000, init);
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Cannot reach external Qdrant at ${baseUrl}.\n` +
|
||||
"Verify that QDRANT_URL (or QDRANT_HOST/QDRANT_PORT) is correct and the server is reachable.",
|
||||
"Verify that QDRANT_URL (or QDRANT_HOST/QDRANT_PORT) and QDRANT_API_KEY (if required) are correct and the server is reachable.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -348,12 +356,16 @@ export async function ensureOllamaContainerReady(onProgress?: InfraProgressCallb
|
||||
|
||||
// ── Shared ────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Wait for an HTTP service to respond with 200 */
|
||||
async function waitForService(url: string, serviceName: string, retries = 30, delayMs = 1000): Promise<void> {
|
||||
/** Wait for an HTTP service to respond with 200.
|
||||
*
|
||||
* `init` is forwarded to fetch(), allowing callers to attach headers (e.g. an
|
||||
* `api-key` header for Qdrant Cloud, whose /healthz endpoint requires auth).
|
||||
*/
|
||||
async function waitForService(url: string, serviceName: string, retries = 30, delayMs = 1000, init?: RequestInit): Promise<void> {
|
||||
logger.info(`Waiting for ${serviceName} to be ready`, { url });
|
||||
for (let i = 0; i < retries; i++) {
|
||||
try {
|
||||
const resp = await fetch(url);
|
||||
const resp = await fetch(url, init);
|
||||
if (resp.ok) {
|
||||
logger.info(`${serviceName} is ready`);
|
||||
return;
|
||||
|
||||
@@ -397,4 +397,49 @@ describe("ensureQdrantReady external mode", () => {
|
||||
|
||||
fetchSpy.mockRestore();
|
||||
}, 30_000);
|
||||
|
||||
it("sends api-key header to /healthz when QDRANT_API_KEY is configured", async () => {
|
||||
const docker = await loadDockerWithExternalMode({
|
||||
QDRANT_URL: "https://cloud-qdrant.example:6333",
|
||||
QDRANT_HOST: "cloud-qdrant.example",
|
||||
QDRANT_API_KEY: "secret-key-xyz",
|
||||
});
|
||||
|
||||
const fetchSpy = vi
|
||||
.spyOn(globalThis, "fetch")
|
||||
.mockResolvedValue({ ok: true } as Response);
|
||||
|
||||
const result = await docker.ensureQdrantReady();
|
||||
expect(result).toEqual({ started: false, pulled: false });
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledWith(
|
||||
"https://cloud-qdrant.example:6333/healthz",
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({ "api-key": "secret-key-xyz" }),
|
||||
}),
|
||||
);
|
||||
|
||||
fetchSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("omits api-key header when QDRANT_API_KEY is not set", async () => {
|
||||
const docker = await loadDockerWithExternalMode({
|
||||
QDRANT_URL: "http://local-qdrant:6333",
|
||||
QDRANT_HOST: "local-qdrant",
|
||||
// QDRANT_API_KEY intentionally undefined (matches local self-hosted Qdrant)
|
||||
});
|
||||
|
||||
const fetchSpy = vi
|
||||
.spyOn(globalThis, "fetch")
|
||||
.mockResolvedValue({ ok: true } as Response);
|
||||
|
||||
await docker.ensureQdrantReady();
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledWith(
|
||||
"http://local-qdrant:6333/healthz",
|
||||
undefined,
|
||||
);
|
||||
|
||||
fetchSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user