fix(db): always bypass system proxy for all HTTP-based database drivers

Replace per-driver proxy bypass logic with a shared http_client_builder()
that always calls .no_proxy(). Previously ES and rqlite only bypassed for
localhost, causing remote connections to be routed through system proxy
(like Clash Verge) and failing — the same issue #922 fixed for ClickHouse.
This commit is contained in:
t8y2
2026-06-09 16:42:04 +08:00
parent 46eaaed8f1
commit ee65fa660d
4 changed files with 13 additions and 47 deletions
+3 -4
View File
@@ -3,7 +3,7 @@ use serde::Deserialize;
use std::fs;
use std::time::{Duration, Instant};
use super::with_connection_timeout;
use super::{http_client_builder, with_connection_timeout};
use crate::query::MAX_ROWS;
use crate::sql::starts_with_executable_sql_keyword;
use crate::types::{ColumnInfo, DatabaseInfo, QueryResult, TableInfo};
@@ -17,8 +17,7 @@ pub struct ChClient {
impl ChClient {
pub fn new(url: &str, username: Option<String>, password: Option<String>, timeout: Duration) -> Self {
let http =
HttpClient::builder().connect_timeout(timeout).no_proxy().build().unwrap_or_else(|_| HttpClient::new());
let http = http_client_builder(timeout).build().unwrap_or_else(|_| HttpClient::new());
Self { http, base_url: url.trim_end_matches('/').to_string(), username, password }
}
@@ -29,7 +28,7 @@ impl ChClient {
ca_cert_path: Option<&str>,
timeout: Duration,
) -> Result<Self, String> {
let mut builder = HttpClient::builder().connect_timeout(timeout).no_proxy();
let mut builder = http_client_builder(timeout);
if let Some(path) = ca_cert_path.map(str::trim).filter(|path| !path.is_empty()) {
let path = expand_cert_path(path);
let cert_bytes =
+3 -27
View File
@@ -5,7 +5,7 @@ use std::collections::HashSet;
use std::error::Error;
use std::time::Duration;
use super::with_connection_timeout;
use super::{http_client_builder, with_connection_timeout};
use crate::db::mongo_driver::MongoDocumentResult;
pub struct EsClient {
@@ -28,11 +28,7 @@ impl EsClient {
(Some(u), Some(p)) if !u.is_empty() => Some((u.to_string(), p.to_string())),
_ => None,
};
let mut builder =
HttpClient::builder().connect_timeout(timeout).danger_accept_invalid_certs(accept_invalid_certs);
if elasticsearch_should_bypass_system_proxy(&base_url) {
builder = builder.no_proxy();
}
let builder = http_client_builder(timeout).danger_accept_invalid_certs(accept_invalid_certs);
let http = builder.build().unwrap_or_else(|_| HttpClient::new());
let fallback_base_urls = elasticsearch_base_url_fallbacks(&base_url);
Self { http, base_url, fallback_base_urls, auth }
@@ -166,17 +162,6 @@ fn elasticsearch_base_url_fallbacks(base_url: &str) -> Vec<String> {
}
}
fn elasticsearch_should_bypass_system_proxy(base_url: &str) -> bool {
let Ok(parsed) = reqwest::Url::parse(base_url) else {
return false;
};
let Some(host) = parsed.host_str() else {
return false;
};
let host = host.trim_matches(['[', ']']);
host.eq_ignore_ascii_case("localhost") || host.parse::<std::net::IpAddr>().is_ok_and(|ip| ip.is_loopback())
}
fn redact_elasticsearch_url(url: &str) -> String {
let Ok(mut parsed) = reqwest::Url::parse(url) else {
return url.to_string();
@@ -1149,8 +1134,7 @@ fn parse_aggregations(aggs: &serde_json::Map<String, serde_json::Value>) -> (Vec
#[cfg(test)]
mod tests {
use super::{
elasticsearch_accept_invalid_certs, elasticsearch_base_url_fallbacks, elasticsearch_should_bypass_system_proxy,
redact_elasticsearch_url, EsClient,
elasticsearch_accept_invalid_certs, elasticsearch_base_url_fallbacks, redact_elasticsearch_url, EsClient,
};
use std::time::Duration;
@@ -1200,12 +1184,4 @@ mod tests {
"https://user:password@localhost:9200"
);
}
#[test]
fn elasticsearch_only_bypasses_system_proxy_for_loopback_hosts() {
assert!(elasticsearch_should_bypass_system_proxy("https://localhost:9200"));
assert!(elasticsearch_should_bypass_system_proxy("https://127.0.0.1:9200"));
assert!(elasticsearch_should_bypass_system_proxy("https://[::1]:9200"));
assert!(!elasticsearch_should_bypass_system_proxy("https://search.example.com:9200"));
}
}
+5
View File
@@ -16,6 +16,7 @@ pub mod sqlserver;
pub mod ssh_tunnel;
pub mod transport_layer_tunnel;
use reqwest::ClientBuilder;
use std::future::Future;
use std::time::Duration;
@@ -30,6 +31,10 @@ pub fn connection_timeout() -> Duration {
Duration::from_secs(CONNECTION_TIMEOUT_SECS)
}
pub fn http_client_builder(timeout: Duration) -> ClientBuilder {
reqwest::Client::builder().connect_timeout(timeout).no_proxy()
}
const JS_MAX_SAFE_INTEGER: i64 = 9_007_199_254_740_991;
pub fn safe_i64_to_json(v: i64) -> serde_json::Value {
+2 -16
View File
@@ -2,7 +2,7 @@ use reqwest::Client as HttpClient;
use serde::Deserialize;
use std::time::{Duration, Instant};
use super::with_connection_timeout;
use super::{http_client_builder, with_connection_timeout};
use crate::sql::starts_with_executable_sql_keyword;
use crate::types::{
ColumnInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, ObjectSource, ObjectSourceKind, QueryResult, TableInfo,
@@ -26,13 +26,10 @@ impl RqliteClient {
tls_enabled: bool,
timeout: Duration,
) -> Result<Self, String> {
let mut builder = HttpClient::builder().connect_timeout(timeout);
let mut builder = http_client_builder(timeout);
if rqlite_accept_invalid_certs(tls_enabled, url_params) {
builder = builder.danger_accept_invalid_certs(true);
}
if rqlite_should_bypass_system_proxy(url) {
builder = builder.no_proxy();
}
let http = builder.build().map_err(|e| format!("Failed to configure rqlite HTTP client: {e}"))?;
let auth = if username.trim().is_empty() { None } else { Some((username.to_string(), password.to_string())) };
Ok(Self {
@@ -411,17 +408,6 @@ fn rqlite_accept_invalid_certs(tls_enabled: bool, url_params: Option<&str>) -> b
})
}
fn rqlite_should_bypass_system_proxy(base_url: &str) -> bool {
let Ok(parsed) = reqwest::Url::parse(base_url) else {
return false;
};
let Some(host) = parsed.host_str() else {
return false;
};
let host = host.trim_matches(['[', ']']);
host.eq_ignore_ascii_case("localhost") || host.parse::<std::net::IpAddr>().is_ok_and(|ip| ip.is_loopback())
}
#[cfg(test)]
mod tests {
use super::*;