fix(mongodb): remove invalid direct connection

This commit is contained in:
t8y2 2026-06-20 22:43:30 +08:00
parent 2c71b9dc06
commit a1e39333a3
3 changed files with 92 additions and 4 deletions

View File

@ -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.`;
}

View File

@ -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::<Vec<_>>().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"));

View File

@ -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 }';