fix(mongodb): support replica set connection URL parsing and timeout

Frontend: add regex-based MongoDB URL parser to handle multi-host URIs
that the WHATWG URL parser rejects. Backend: separate
server_selection_timeout from connect_timeout for multi-host URIs to
prevent topology discovery from being cancelled by tokio timeout.
This commit is contained in:
t8y2
2026-06-09 01:02:11 +08:00
parent 0ce6fde347
commit 80895fe273
3 changed files with 147 additions and 13 deletions
+65 -11
View File
@@ -79,20 +79,71 @@ export function normalizeMongoConnectionString(value: string): string {
const input = value.trim();
if (!input) return input;
let parsed: URL;
try {
parsed = new URL(input);
} catch {
return input;
const mongoMatch = input.match(/^(mongodb(?:\+srv)?):\/\/(?:(.+)@)?/i);
if (!mongoMatch) return input;
const userinfo = mongoMatch[2];
if (!userinfo) return input;
const [username, ...passwordParts] = userinfo.split(":");
const password = passwordParts.join(":");
const encodedUsername = encodeMongoUserInfoPart(username);
const encodedPassword = password ? `:${encodeMongoUserInfoPart(password)}` : "";
return input.replace(/^(mongodb(?:\+srv)?:\/\/)(?:(.+)@)?/i, `$1${encodedUsername}${encodedPassword}@`);
}
function parseMongoUrl(source: string): ParsedConnectionUrl | null {
const match = source.match(/^(mongodb(?:\+srv)?):\/\/(?:(.+)@)?([^/]+)(\/[^?]*)?(\?.*)?$/);
if (!match) return null;
const scheme = match[1].toLowerCase();
const userinfo = match[2] || "";
const hosts = match[3] || "";
const pathname = match[4] || "";
const search = match[5] || "";
const profile = SCHEME_PROFILES[scheme];
if (!profile) return null;
const [username, ...passwordParts] = decodeUrlPart(userinfo).split(":");
const password = passwordParts.join(":");
const firstHost = hosts.split(",")[0];
let host: string;
let port: number;
if (firstHost.startsWith("[")) {
const bracketEnd = firstHost.indexOf("]");
host = firstHost.substring(1, bracketEnd);
port = firstHost.substring(bracketEnd + 1).startsWith(":")
? Number(firstHost.substring(bracketEnd + 2)) || profile.defaultPort
: profile.defaultPort;
} else if (firstHost.includes(":")) {
const colonIdx = firstHost.lastIndexOf(":");
host = firstHost.substring(0, colonIdx);
port = Number(firstHost.substring(colonIdx + 1)) || profile.defaultPort;
} else {
host = firstHost;
port = profile.defaultPort;
}
const scheme = parsed.protocol.replace(/:$/, "").toLowerCase();
if (scheme !== "mongodb" && scheme !== "mongodb+srv") return input;
if (!parsed.username && !parsed.password) return input;
const database = databaseFromPath(pathname);
const urlParams = search.replace(/^\?/, "");
const username = encodeMongoUserInfoPart(parsed.username);
const password = parsed.password ? `:${encodeMongoUserInfoPart(parsed.password)}` : "";
return `${parsed.protocol}//${username}${password}@${parsed.host}${parsed.pathname}${parsed.search}${parsed.hash}`;
return {
dbType: profile.type,
driverProfile: profile.profile,
driverLabel: profile.label,
host,
port,
username,
password,
database,
urlParams,
ssl: scheme === "mongodb+srv",
connectionString: normalizeMongoConnectionString(source),
useMongoUrl: true,
};
}
function databaseFromPath(pathname: string): string | undefined {
@@ -330,6 +381,9 @@ export function parseConnectionUrl(value: string, preferredProfile?: string): Pa
const isJdbcUrl = /^jdbc:/i.test(input);
const source = isJdbcUrl ? input.replace(/^jdbc:/i, "") : input;
const mongoResult = parseMongoUrl(source);
if (mongoResult) return mongoResult;
let parsed: URL;
try {
parsed = new URL(source);