Przeglądaj źródła

fix(qdrant): resolve URL port handling bug for HTTPS URLs (#4992)

Co-authored-by: Ben Ashby <[email protected]>
Ben Ashby 6 miesięcy temu
rodzic
commit
f39da324d4

+ 245 - 3
src/services/code-index/vector-store/__tests__/qdrant-client.spec.ts

@@ -58,7 +58,9 @@ describe("QdrantVectorStore", () => {
 	it("should correctly initialize QdrantClient and collectionName in constructor", () => {
 		expect(QdrantClient).toHaveBeenCalledTimes(1)
 		expect(QdrantClient).toHaveBeenCalledWith({
-			url: mockQdrantUrl,
+			host: "mock-qdrant",
+			https: false,
+			port: 6333,
 			apiKey: mockApiKey,
 			headers: {
 				"User-Agent": "Roo-Code",
@@ -75,7 +77,9 @@ describe("QdrantVectorStore", () => {
 		const vectorStoreWithDefaults = new QdrantVectorStore(mockWorkspacePath, undefined as any, mockVectorSize)
 
 		expect(QdrantClient).toHaveBeenLastCalledWith({
-			url: "http://localhost:6333", // Should use default QDRANT_URL
+			host: "localhost",
+			https: false,
+			port: 6333,
 			apiKey: undefined,
 			headers: {
 				"User-Agent": "Roo-Code",
@@ -87,7 +91,9 @@ describe("QdrantVectorStore", () => {
 		const vectorStoreWithoutKey = new QdrantVectorStore(mockWorkspacePath, mockQdrantUrl, mockVectorSize)
 
 		expect(QdrantClient).toHaveBeenLastCalledWith({
-			url: mockQdrantUrl,
+			host: "mock-qdrant",
+			https: false,
+			port: 6333,
 			apiKey: undefined,
 			headers: {
 				"User-Agent": "Roo-Code",
@@ -95,6 +101,242 @@ describe("QdrantVectorStore", () => {
 		})
 	})
 
+	describe("URL Parsing and Explicit Port Handling", () => {
+		describe("HTTPS URL handling", () => {
+			it("should use explicit port 443 for HTTPS URLs without port (fixes the main bug)", () => {
+				const vectorStore = new QdrantVectorStore(
+					mockWorkspacePath,
+					"https://qdrant.ashbyfam.com",
+					mockVectorSize,
+				)
+				expect(QdrantClient).toHaveBeenLastCalledWith({
+					host: "qdrant.ashbyfam.com",
+					https: true,
+					port: 443,
+					apiKey: undefined,
+					headers: {
+						"User-Agent": "Roo-Code",
+					},
+				})
+				expect((vectorStore as any).qdrantUrl).toBe("https://qdrant.ashbyfam.com")
+			})
+
+			it("should use explicit port for HTTPS URLs with explicit port", () => {
+				const vectorStore = new QdrantVectorStore(mockWorkspacePath, "https://example.com:9000", mockVectorSize)
+				expect(QdrantClient).toHaveBeenLastCalledWith({
+					host: "example.com",
+					https: true,
+					port: 9000,
+					apiKey: undefined,
+					headers: {
+						"User-Agent": "Roo-Code",
+					},
+				})
+				expect((vectorStore as any).qdrantUrl).toBe("https://example.com:9000")
+			})
+
+			it("should use port 443 for HTTPS URLs with paths and query parameters", () => {
+				const vectorStore = new QdrantVectorStore(
+					mockWorkspacePath,
+					"https://example.com/api/v1?key=value",
+					mockVectorSize,
+				)
+				expect(QdrantClient).toHaveBeenLastCalledWith({
+					host: "example.com",
+					https: true,
+					port: 443,
+					apiKey: undefined,
+					headers: {
+						"User-Agent": "Roo-Code",
+					},
+				})
+				expect((vectorStore as any).qdrantUrl).toBe("https://example.com/api/v1?key=value")
+			})
+		})
+
+		describe("HTTP URL handling", () => {
+			it("should use explicit port 80 for HTTP URLs without port", () => {
+				const vectorStore = new QdrantVectorStore(mockWorkspacePath, "http://example.com", mockVectorSize)
+				expect(QdrantClient).toHaveBeenLastCalledWith({
+					host: "example.com",
+					https: false,
+					port: 80,
+					apiKey: undefined,
+					headers: {
+						"User-Agent": "Roo-Code",
+					},
+				})
+				expect((vectorStore as any).qdrantUrl).toBe("http://example.com")
+			})
+
+			it("should use explicit port for HTTP URLs with explicit port", () => {
+				const vectorStore = new QdrantVectorStore(mockWorkspacePath, "http://localhost:8080", mockVectorSize)
+				expect(QdrantClient).toHaveBeenLastCalledWith({
+					host: "localhost",
+					https: false,
+					port: 8080,
+					apiKey: undefined,
+					headers: {
+						"User-Agent": "Roo-Code",
+					},
+				})
+				expect((vectorStore as any).qdrantUrl).toBe("http://localhost:8080")
+			})
+
+			it("should use port 80 for HTTP URLs while preserving paths and query parameters", () => {
+				const vectorStore = new QdrantVectorStore(
+					mockWorkspacePath,
+					"http://example.com/api/v1?key=value",
+					mockVectorSize,
+				)
+				expect(QdrantClient).toHaveBeenLastCalledWith({
+					host: "example.com",
+					https: false,
+					port: 80,
+					apiKey: undefined,
+					headers: {
+						"User-Agent": "Roo-Code",
+					},
+				})
+				expect((vectorStore as any).qdrantUrl).toBe("http://example.com/api/v1?key=value")
+			})
+		})
+
+		describe("Hostname handling", () => {
+			it("should convert hostname to http with port 80", () => {
+				const vectorStore = new QdrantVectorStore(mockWorkspacePath, "qdrant.example.com", mockVectorSize)
+				expect(QdrantClient).toHaveBeenLastCalledWith({
+					host: "qdrant.example.com",
+					https: false,
+					port: 80,
+					apiKey: undefined,
+					headers: {
+						"User-Agent": "Roo-Code",
+					},
+				})
+				expect((vectorStore as any).qdrantUrl).toBe("http://qdrant.example.com")
+			})
+
+			it("should handle hostname:port format with explicit port", () => {
+				const vectorStore = new QdrantVectorStore(mockWorkspacePath, "localhost:6333", mockVectorSize)
+				expect(QdrantClient).toHaveBeenLastCalledWith({
+					host: "localhost",
+					https: false,
+					port: 6333,
+					apiKey: undefined,
+					headers: {
+						"User-Agent": "Roo-Code",
+					},
+				})
+				expect((vectorStore as any).qdrantUrl).toBe("http://localhost:6333")
+			})
+
+			it("should handle explicit HTTP URLs correctly", () => {
+				const vectorStore = new QdrantVectorStore(mockWorkspacePath, "http://localhost:9000", mockVectorSize)
+				expect(QdrantClient).toHaveBeenLastCalledWith({
+					host: "localhost",
+					https: false,
+					port: 9000,
+					apiKey: undefined,
+					headers: {
+						"User-Agent": "Roo-Code",
+					},
+				})
+				expect((vectorStore as any).qdrantUrl).toBe("http://localhost:9000")
+			})
+		})
+
+		describe("IP address handling", () => {
+			it("should convert IP address to http with port 80", () => {
+				const vectorStore = new QdrantVectorStore(mockWorkspacePath, "192.168.1.100", mockVectorSize)
+				expect(QdrantClient).toHaveBeenLastCalledWith({
+					host: "192.168.1.100",
+					https: false,
+					port: 80,
+					apiKey: undefined,
+					headers: {
+						"User-Agent": "Roo-Code",
+					},
+				})
+				expect((vectorStore as any).qdrantUrl).toBe("http://192.168.1.100")
+			})
+
+			it("should handle IP:port format with explicit port", () => {
+				const vectorStore = new QdrantVectorStore(mockWorkspacePath, "192.168.1.100:6333", mockVectorSize)
+				expect(QdrantClient).toHaveBeenLastCalledWith({
+					host: "192.168.1.100",
+					https: false,
+					port: 6333,
+					apiKey: undefined,
+					headers: {
+						"User-Agent": "Roo-Code",
+					},
+				})
+				expect((vectorStore as any).qdrantUrl).toBe("http://192.168.1.100:6333")
+			})
+		})
+
+		describe("Edge cases", () => {
+			it("should handle undefined URL with host-based config", () => {
+				const vectorStore = new QdrantVectorStore(mockWorkspacePath, undefined as any, mockVectorSize)
+				expect(QdrantClient).toHaveBeenLastCalledWith({
+					host: "localhost",
+					https: false,
+					port: 6333,
+					apiKey: undefined,
+					headers: {
+						"User-Agent": "Roo-Code",
+					},
+				})
+				expect((vectorStore as any).qdrantUrl).toBe("http://localhost:6333")
+			})
+
+			it("should handle empty string URL with host-based config", () => {
+				const vectorStore = new QdrantVectorStore(mockWorkspacePath, "", mockVectorSize)
+				expect(QdrantClient).toHaveBeenLastCalledWith({
+					host: "localhost",
+					https: false,
+					port: 6333,
+					apiKey: undefined,
+					headers: {
+						"User-Agent": "Roo-Code",
+					},
+				})
+				expect((vectorStore as any).qdrantUrl).toBe("http://localhost:6333")
+			})
+
+			it("should handle whitespace-only URL with host-based config", () => {
+				const vectorStore = new QdrantVectorStore(mockWorkspacePath, "   ", mockVectorSize)
+				expect(QdrantClient).toHaveBeenLastCalledWith({
+					host: "localhost",
+					https: false,
+					port: 6333,
+					apiKey: undefined,
+					headers: {
+						"User-Agent": "Roo-Code",
+					},
+				})
+				expect((vectorStore as any).qdrantUrl).toBe("http://localhost:6333")
+			})
+		})
+
+		describe("Invalid URL fallback", () => {
+			it("should treat invalid URLs as hostnames with port 80", () => {
+				const vectorStore = new QdrantVectorStore(mockWorkspacePath, "invalid-url-format", mockVectorSize)
+				expect(QdrantClient).toHaveBeenLastCalledWith({
+					host: "invalid-url-format",
+					https: false,
+					port: 80,
+					apiKey: undefined,
+					headers: {
+						"User-Agent": "Roo-Code",
+					},
+				})
+				expect((vectorStore as any).qdrantUrl).toBe("http://invalid-url-format")
+			})
+		})
+	})
+
 	describe("initialize", () => {
 		it("should create a new collection if none exists and return true", async () => {
 			// Mock getCollection to throw a 404-like error

+ 92 - 8
src/services/code-index/vector-store/qdrant-client.ts

@@ -24,14 +24,54 @@ export class QdrantVectorStore implements IVectorStore {
 	 * @param url Optional URL to the Qdrant server
 	 */
 	constructor(workspacePath: string, url: string, vectorSize: number, apiKey?: string) {
-		this.qdrantUrl = url || "http://localhost:6333"
-		this.client = new QdrantClient({
-			url: this.qdrantUrl,
-			apiKey,
-			headers: {
-				"User-Agent": "Roo-Code",
-			},
-		})
+		// Parse the URL to determine the appropriate QdrantClient configuration
+		const parsedUrl = this.parseQdrantUrl(url)
+
+		// Store the resolved URL for our property
+		this.qdrantUrl = parsedUrl
+
+		try {
+			const urlObj = new URL(parsedUrl)
+
+			// Always use host-based configuration with explicit ports to avoid QdrantClient defaults
+			let port: number
+			let useHttps: boolean
+
+			if (urlObj.port) {
+				// Explicit port specified - use it and determine protocol
+				port = Number(urlObj.port)
+				useHttps = urlObj.protocol === "https:"
+			} else {
+				// No explicit port - use protocol defaults
+				if (urlObj.protocol === "https:") {
+					port = 443
+					useHttps = true
+				} else {
+					// http: or other protocols default to port 80
+					port = 80
+					useHttps = false
+				}
+			}
+
+			this.client = new QdrantClient({
+				host: urlObj.hostname,
+				https: useHttps,
+				port: port,
+				apiKey,
+				headers: {
+					"User-Agent": "Roo-Code",
+				},
+			})
+		} catch (urlError) {
+			// If URL parsing fails, fall back to URL-based config
+			this.client = new QdrantClient({
+				url: parsedUrl,
+				apiKey,
+				headers: {
+					"User-Agent": "Roo-Code",
+				},
+			})
+		}
 
 		// Generate collection name from workspace path
 		const hash = createHash("sha256").update(workspacePath).digest("hex")
@@ -39,6 +79,50 @@ export class QdrantVectorStore implements IVectorStore {
 		this.collectionName = `ws-${hash.substring(0, 16)}`
 	}
 
+	/**
+	 * Parses and normalizes Qdrant server URLs to handle various input formats
+	 * @param url Raw URL input from user
+	 * @returns Properly formatted URL for QdrantClient
+	 */
+	private parseQdrantUrl(url: string | undefined): string {
+		// Handle undefined/null/empty cases
+		if (!url || url.trim() === "") {
+			return "http://localhost:6333"
+		}
+
+		const trimmedUrl = url.trim()
+
+		// Check if it starts with a protocol
+		if (!trimmedUrl.startsWith("http://") && !trimmedUrl.startsWith("https://") && !trimmedUrl.includes("://")) {
+			// No protocol - treat as hostname
+			return this.parseHostname(trimmedUrl)
+		}
+
+		try {
+			// Attempt to parse as complete URL - return as-is, let constructor handle ports
+			const parsedUrl = new URL(trimmedUrl)
+			return trimmedUrl
+		} catch {
+			// Failed to parse as URL - treat as hostname
+			return this.parseHostname(trimmedUrl)
+		}
+	}
+
+	/**
+	 * Handles hostname-only inputs
+	 * @param hostname Raw hostname input
+	 * @returns Properly formatted URL with http:// prefix
+	 */
+	private parseHostname(hostname: string): string {
+		if (hostname.includes(":")) {
+			// Has port - add http:// prefix if missing
+			return hostname.startsWith("http") ? hostname : `http://${hostname}`
+		} else {
+			// No port - add http:// prefix without port (let constructor handle port assignment)
+			return `http://${hostname}`
+		}
+	}
+
 	private async getCollectionInfo(): Promise<Schemas["CollectionInfo"] | null> {
 		try {
 			const collectionInfo = await this.client.getCollection(this.collectionName)