mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
fix(qdrant): resolve URL port handling bug for HTTPS URLs (#4992)
Co-authored-by: Ben Ashby <bashby@ximasoftware.com>
This commit is contained in:
parent
8eb147346d
commit
f39da324d4
2 changed files with 337 additions and 11 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue