From 431cba20e9179bbeb72a0ca74161d54dfa500e3f Mon Sep 17 00:00:00 2001 From: Utkarsh Date: Sat, 28 Mar 2026 21:50:36 +0530 Subject: [PATCH] fix(ts): work around Qdrant Cloud "Illegal host" error (#4565) Co-authored-by: utkarsh240799 Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: kartik-mem0 --- mem0-ts/src/oss/src/vector_stores/qdrant.ts | 21 +++ mem0-ts/src/oss/tests/qdrant-url-port.test.ts | 170 ++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 mem0-ts/src/oss/tests/qdrant-url-port.test.ts diff --git a/mem0-ts/src/oss/src/vector_stores/qdrant.ts b/mem0-ts/src/oss/src/vector_stores/qdrant.ts index 699760465..81c6faea8 100644 --- a/mem0-ts/src/oss/src/vector_stores/qdrant.ts +++ b/mem0-ts/src/oss/src/vector_stores/qdrant.ts @@ -4,6 +4,20 @@ import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; import * as fs from "fs"; interface QdrantConfig extends VectorStoreConfig { + /** + * Pre-configured QdrantClient instance. If using Qdrant Cloud, you must pass + * `port` explicitly when constructing the client to avoid "Illegal host" errors + * caused by a known upstream bug (qdrant/qdrant-js#59). + * + * @example + * ```typescript + * const client = new QdrantClient({ + * url: "https://xxx.cloud.qdrant.io:6333", + * port: 6333, + * apiKey: "xxx", + * }); + * ``` + */ client?: QdrantClient; host?: string; port?: number; @@ -44,6 +58,13 @@ export class Qdrant implements VectorStore { } if (config.url) { params.url = config.url; + // Workaround for qdrant/qdrant-js#59: explicitly pass port to avoid "Illegal host" error + try { + const parsedUrl = new URL(config.url); + params.port = parsedUrl.port ? parseInt(parsedUrl.port, 10) : 6333; + } catch (_) { + params.port = 6333; + } } if (config.host && config.port) { params.host = config.host; diff --git a/mem0-ts/src/oss/tests/qdrant-url-port.test.ts b/mem0-ts/src/oss/tests/qdrant-url-port.test.ts new file mode 100644 index 000000000..ae86ea471 --- /dev/null +++ b/mem0-ts/src/oss/tests/qdrant-url-port.test.ts @@ -0,0 +1,170 @@ +/// +/** + * Tests for Qdrant Cloud URL port extraction workaround (qdrant/qdrant-js#59). + * + * Verifies that when a `url` config is provided, the port is extracted and + * passed explicitly to QdrantClient to avoid "Illegal host" errors. + */ + +jest.setTimeout(15000); + +let capturedParams: Record | undefined; + +// Mock QdrantClient to capture constructor params +jest.mock("@qdrant/js-client-rest", () => { + return { + QdrantClient: jest.fn().mockImplementation((params: any) => { + capturedParams = params; + // Return a mock client that satisfies initialize() + return { + createCollection: jest.fn().mockResolvedValue(undefined), + getCollection: jest.fn().mockResolvedValue({ + config: { params: { vectors: { size: 768 } } }, + }), + scroll: jest.fn().mockResolvedValue({ points: [] }), + upsert: jest.fn().mockResolvedValue(undefined), + retrieve: jest.fn().mockResolvedValue([]), + search: jest.fn().mockResolvedValue([]), + delete: jest.fn().mockResolvedValue(undefined), + deleteCollection: jest.fn().mockResolvedValue(undefined), + }; + }), + }; +}); + +import { Qdrant } from "../src/vector_stores/qdrant"; + +beforeEach(() => { + capturedParams = undefined; + (require("@qdrant/js-client-rest").QdrantClient as jest.Mock).mockClear(); +}); + +describe("Qdrant URL port extraction (qdrant/qdrant-js#59 workaround)", () => { + it("extracts port from HTTPS URL with explicit port", () => { + new Qdrant({ + url: "https://my-cluster.us-west-1-0.aws.cloud.qdrant.io:6333", + apiKey: "test-key", + collectionName: "test", + embeddingModelDims: 768, + dimension: 768, + }); + + expect(capturedParams).toBeDefined(); + expect(capturedParams!.url).toBe( + "https://my-cluster.us-west-1-0.aws.cloud.qdrant.io:6333", + ); + expect(capturedParams!.port).toBe(6333); + expect(capturedParams!.apiKey).toBe("test-key"); + }); + + it("extracts port from HTTP URL with explicit port", () => { + new Qdrant({ + url: "http://localhost:6333", + collectionName: "test", + embeddingModelDims: 768, + dimension: 768, + }); + + expect(capturedParams).toBeDefined(); + expect(capturedParams!.url).toBe("http://localhost:6333"); + expect(capturedParams!.port).toBe(6333); + }); + + it("defaults to port 6333 when HTTPS URL has no explicit port", () => { + new Qdrant({ + url: "https://my-cluster.cloud.qdrant.io", + apiKey: "test-key", + collectionName: "test", + embeddingModelDims: 768, + dimension: 768, + }); + + expect(capturedParams).toBeDefined(); + expect(capturedParams!.url).toBe("https://my-cluster.cloud.qdrant.io"); + expect(capturedParams!.port).toBe(6333); + }); + + it("defaults to port 6333 when HTTP URL has no explicit port", () => { + new Qdrant({ + url: "http://localhost", + collectionName: "test", + embeddingModelDims: 768, + dimension: 768, + }); + + expect(capturedParams).toBeDefined(); + expect(capturedParams!.port).toBe(6333); + }); + + it("host+port config overrides URL-extracted port", () => { + new Qdrant({ + url: "https://my-cluster.cloud.qdrant.io:6333", + host: "custom-host", + port: 9999, + apiKey: "test-key", + collectionName: "test", + embeddingModelDims: 768, + dimension: 768, + }); + + expect(capturedParams).toBeDefined(); + expect(capturedParams!.host).toBe("custom-host"); + expect(capturedParams!.port).toBe(9999); + }); + + it("handles invalid URL gracefully without crashing", () => { + expect(() => { + new Qdrant({ + url: "not-a-valid-url", + collectionName: "test", + embeddingModelDims: 768, + dimension: 768, + }); + }).not.toThrow(); + + expect(capturedParams).toBeDefined(); + expect(capturedParams!.url).toBe("not-a-valid-url"); + expect(capturedParams!.port).toBe(6333); + }); + + it("does not pass port when using pre-configured client", () => { + const mockClient: any = { + createCollection: jest.fn().mockResolvedValue(undefined), + getCollection: jest.fn().mockResolvedValue({ + config: { params: { vectors: { size: 768 } } }, + }), + scroll: jest.fn().mockResolvedValue({ points: [] }), + upsert: jest.fn().mockResolvedValue(undefined), + retrieve: jest.fn().mockResolvedValue([]), + search: jest.fn().mockResolvedValue([]), + delete: jest.fn().mockResolvedValue(undefined), + deleteCollection: jest.fn().mockResolvedValue(undefined), + }; + + new Qdrant({ + client: mockClient, + collectionName: "test", + embeddingModelDims: 768, + dimension: 768, + }); + + // QdrantClient constructor should NOT have been called + expect( + require("@qdrant/js-client-rest").QdrantClient, + ).not.toHaveBeenCalled(); + }); + + it("defaults to 6333 when HTTPS URL uses default port 443", () => { + new Qdrant({ + url: "https://my-cluster.cloud.qdrant.io:443", + apiKey: "test-key", + collectionName: "test", + embeddingModelDims: 768, + dimension: 768, + }); + + // 443 is default for HTTPS, so URL.port returns empty string — we default to 6333 + expect(capturedParams).toBeDefined(); + expect(capturedParams!.port).toBe(6333); + }); +});