From a1e39333a3fe34cb310e8fcaa302e46378dff895 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Sat, 20 Jun 2026 22:43:30 +0800 Subject: [PATCH] fix(mongodb): remove invalid direct connection --- .../desktop/src/lib/mongoConnectionOptions.ts | 4 + crates/dbx-core/src/models/connection.rs | 86 ++++++++++++++++++- .../app-tests/mongoConnectionOptions.test.ts | 6 ++ 3 files changed, 92 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/lib/mongoConnectionOptions.ts b/apps/desktop/src/lib/mongoConnectionOptions.ts index e16634170..49bccc899 100644 --- a/apps/desktop/src/lib/mongoConnectionOptions.ts +++ b/apps/desktop/src/lib/mongoConnectionOptions.ts @@ -15,6 +15,10 @@ export function setMongoUrlParam(urlParams: string | undefined, key: string, val } export function mongodbAuthFailureHint(message: string): string { + if (message.includes("cannot specify multiple seeds with directConnection=true")) { + return `${message}\n\nMongoDB directConnection=true can only be used with a single host. Remove directConnection=true when using a multi-host replica set URL, or keep only one reachable host if you need a direct connection.`; + } + if (message.includes("no records found") && message.includes("_mongodb._tcp.")) { return `${message}\n\nMongoDB SRV URLs require a DNS hostname with SRV records. For an IP address or normal host:port endpoint, use mongodb://host:port instead of mongodb+srv://host.`; } diff --git a/crates/dbx-core/src/models/connection.rs b/crates/dbx-core/src/models/connection.rs index 33386592b..c697bf939 100644 --- a/crates/dbx-core/src/models/connection.rs +++ b/crates/dbx-core/src/models/connection.rs @@ -713,10 +713,11 @@ impl ConnectionConfig { DatabaseType::MongoDb => { let is_tunneled = host != self.host.as_str() || port != self.port; if let Some(cs) = self.connection_string.as_deref().filter(|s| !s.is_empty()) { + let cs = normalize_mongo_uri_direct_connection(cs); if is_tunneled { - return rewrite_mongo_uri_host(cs, &host, port); + return rewrite_mongo_uri_host(&cs, &host, port); } - return cs.to_string(); + return cs; } let mut suffix = if params.is_empty() { String::new() } else { format!("?{params}") }; if is_tunneled && !suffix.contains("directConnection=") { @@ -840,10 +841,11 @@ impl ConnectionConfig { DatabaseType::MongoDb => { let is_tunneled = host != self.host.as_str() || port != self.port; if let Some(cs) = self.connection_string.as_deref().filter(|s| !s.is_empty()) { + let cs = normalize_mongo_uri_direct_connection(cs); if is_tunneled { - return rewrite_mongo_uri_host(cs, &host, port); + return rewrite_mongo_uri_host(&cs, &host, port); } - return cs.to_string(); + return cs; } let mut suffix = if params.is_empty() { String::new() } else { format!("?{params}") }; if is_tunneled && !suffix.contains("directConnection=") { @@ -1125,6 +1127,58 @@ fn normalize_mongo_url_params(value: &str, force_tls: bool) -> String { parts.join("&") } +fn normalize_mongo_uri_direct_connection(uri: &str) -> String { + if !mongo_uri_has_multiple_seeds(uri) || !mongo_uri_has_direct_connection_true(uri) { + return uri.to_string(); + } + + let (before_fragment, fragment) = + uri.split_once('#').map(|(base, fragment)| (base, Some(fragment))).unwrap_or((uri, None)); + let Some((base, query)) = before_fragment.split_once('?') else { + return uri.to_string(); + }; + let params = + query.split('&').filter(|part| !mongo_url_param_is_direct_connection_true(part)).collect::>().join("&"); + + let mut normalized = if params.is_empty() { base.to_string() } else { format!("{base}?{params}") }; + if let Some(fragment) = fragment { + normalized.push('#'); + normalized.push_str(fragment); + } + normalized +} + +fn mongo_uri_has_multiple_seeds(uri: &str) -> bool { + mongo_uri_host_section(uri) + .map(|hosts| hosts.split(',').filter(|host| !host.trim().is_empty()).count() > 1) + .unwrap_or(false) +} + +fn mongo_uri_host_section(uri: &str) -> Option<&str> { + let rest = uri.strip_prefix("mongodb://").or_else(|| uri.strip_prefix("mongodb+srv://"))?; + let authority = rest.split('/').next()?.split('?').next().unwrap_or(rest); + Some(match authority.rfind('@') { + Some(idx) => &authority[idx + 1..], + None => authority, + }) +} + +fn mongo_uri_has_direct_connection_true(uri: &str) -> bool { + uri.split_once('?') + .map(|(_, query)| { + query.split('#').next().unwrap_or("").split('&').any(mongo_url_param_is_direct_connection_true) + }) + .unwrap_or(false) +} + +fn mongo_url_param_is_direct_connection_true(part: &str) -> bool { + let Some((key, value)) = part.split_once('=') else { + return false; + }; + percent_decode_str(key).decode_utf8_lossy().eq_ignore_ascii_case("directConnection") + && percent_decode_str(value).decode_utf8_lossy().eq_ignore_ascii_case("true") +} + fn normalize_postgres_url_params(value: &str, force_tls: bool) -> String { let value = value.trim_start_matches('?'); @@ -2147,6 +2201,30 @@ mod tests { assert_eq!(url, "mongodb://read:pass@host1:27017,host2:27017/admin?replicaSet=rs0"); } + #[test] + fn mongodb_multi_seed_connection_string_removes_direct_connection_true() { + let mut config = mongodb_config("root", "secret", Some("admin")); + config.connection_string = Some( + "mongodb://read:pass@host1:27017,host2:27017/admin?directConnection=true&replicaSet=rs0&authSource=admin" + .to_string(), + ); + + let url = config.connection_url(); + + assert_eq!(url, "mongodb://read:pass@host1:27017,host2:27017/admin?replicaSet=rs0&authSource=admin"); + } + + #[test] + fn mongodb_single_seed_connection_string_keeps_direct_connection_true() { + let mut config = mongodb_config("root", "secret", Some("admin")); + config.connection_string = + Some("mongodb://read:pass@host1:27017/admin?directConnection=true&authSource=admin".to_string()); + + let url = config.connection_url(); + + assert_eq!(url, "mongodb://read:pass@host1:27017/admin?directConnection=true&authSource=admin"); + } + #[test] fn mongodb_form_url_adds_direct_connection_when_tunneled() { let mut config = mongodb_config("root", "secret", Some("admin")); diff --git a/packages/app-tests/mongoConnectionOptions.test.ts b/packages/app-tests/mongoConnectionOptions.test.ts index 6190329aa..697a47e63 100644 --- a/packages/app-tests/mongoConnectionOptions.test.ts +++ b/packages/app-tests/mongoConnectionOptions.test.ts @@ -29,6 +29,12 @@ test("adds a MongoDB URL encoding hint for reserved password characters", () => assert.match(mongodbAuthFailureHint(message), /@ becomes %40/); }); +test("adds a MongoDB directConnection multi-host hint", () => { + const message = "MongoDB connection failed: Kind: An invalid argument was provided: cannot specify multiple seeds with directConnection=true"; + + assert.match(mongodbAuthFailureHint(message), /Remove directConnection=true when using a multi-host replica set URL/); +}); + test("adds a MongoDB SRV DNS hint for IP endpoints", () => { const message = 'MongoDB connection failed: Kind: An error occurred during DNS resolution: DNS error: no records found for Query { name: Name("_mongodb._tcp.172.17.0.1."), query_type: SRV }';