fix(mysql): add TLS failure hint

This commit is contained in:
zipg 2026-07-06 10:47:57 +08:00 committed by GitHub
parent 38f9893425
commit 95397f7152
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 106 additions and 1 deletions

View File

@ -37,6 +37,7 @@ import { agentDriverInstallKey, appendAgentDriverUpdateHint, hasAgentDriverUpdat
import { prestoSqlBuiltinDriverPaths } from "@/lib/database/prestoSqlBuiltinDriver";
import { SQLITE_DATABASE_FILE_EXTENSIONS } from "@/lib/database/databaseFileDetection";
import { connectionAttemptOriginalErrorMessage, connectionAttemptTimeoutMessage, connectionAttemptTimeoutMs } from "@/lib/connection/connectionAttemptTimeout";
import { appendConnectionErrorHints } from "@/lib/connection/connectionErrorHints";
import { driverInstallProgressPercent, type DriverInstallProgress } from "@/lib/connection/driverInstallProgressUi";
import { ArrowLeft, ArrowDown, ArrowUp, CheckSquare, ChevronRight, CircleHelp, Copy, ExternalLink, FilePlus2, FolderOpen, GripVertical, Grid3X3, KeyRound, Link2, List, ListFilter, Loader2, Pencil, Pipette, Plus, Search, ShieldCheck, Square, Trash2 } from "@lucide/vue";
import { buildDraftVisibleDatabasesConnectionId, connectionCanChooseVisibleDatabases, initialVisibleDatabaseSelection, visibleDatabaseSelectionIsStale } from "@/lib/connection/connectionVisibleDatabases";
@ -918,6 +919,7 @@ function errorMessage(error: unknown): string {
}
function connectionErrorWithDriverUpdateHint(config: ConnectionConfig, message: string): string {
message = appendConnectionErrorHints(config, message, t);
if (!hasAgentDriverUpdate(config.db_type, agentDrivers.value, config.driver_profile)) return message;
return appendAgentDriverUpdateHint(message, t("connection.agentDriverUpdateConnectionHint"));
}
@ -3039,7 +3041,7 @@ async function save() {
const message = String(e?.message || e);
if (message.includes(CONNECTION_ATTEMPT_CANCELLED_MESSAGE)) return;
if (config.one_time) void store.removeConnection(config.id);
emit("connectFailed", mongodbAuthFailureHint(message));
emit("connectFailed", appendConnectionErrorHints(config, mongodbAuthFailureHint(message), t));
});
return;
}

View File

@ -215,6 +215,7 @@ export default {
mysqlClientCertPlaceholder: "/path/to/client.crt",
mysqlClientKeyPlaceholder: "/path/to/client.key",
mysqlClientCertHint: "Client certificate and private key must be provided together when MySQL requires mTLS.",
mysqlTlsConnectionFailureHint: "If the MySQL server does not require TLS, edit the connection, set TLS Mode to Disabled, and try again.",
mysqlClientCertBrowse: "Choose client certificate",
mysqlClientKeyBrowse: "Choose client private key",
postgresSslMode: "TLS Mode",

View File

@ -217,6 +217,7 @@ export default withEnglishFallback({
mysqlClientCertPlaceholder: "/ruta/a/client.crt",
mysqlClientKeyPlaceholder: "/ruta/a/client.key",
mysqlClientCertHint: "El certificado y la clave privada deben indicarse juntos si MySQL requiere mTLS.",
mysqlTlsConnectionFailureHint: "Si el servidor MySQL no requiere TLS, edita la conexión, establece el Modo TLS en Deshabilitado e inténtalo de nuevo.",
mysqlClientCertBrowse: "Elegir certificado cliente",
mysqlClientKeyBrowse: "Elegir clave privada cliente",
postgresSslMode: "Modo TLS",

View File

@ -216,6 +216,7 @@ export default withEnglishFallback({
mysqlClientCertPlaceholder: "/percorso/per/client.crt",
mysqlClientKeyPlaceholder: "/percorso/per/client.key",
mysqlClientCertHint: "Il certificato client e la chiave privata devono essere forniti insieme quando MySQL richiede mTLS.",
mysqlTlsConnectionFailureHint: "Se il server MySQL non richiede TLS, modifica la connessione, imposta la modalità TLS su Disabilitata e riprova.",
mysqlClientCertBrowse: "Scegli certificato client",
mysqlClientKeyBrowse: "Scegli chiave privata client",
postgresSslMode: "Modalità TLS",

View File

@ -216,6 +216,7 @@ export default withEnglishFallback({
mysqlClientCertPlaceholder: "/path/to/client.crt",
mysqlClientKeyPlaceholder: "/path/to/client.key",
mysqlClientCertHint: "MySQLがmTLSを要求する場合、クライアント証明書と秘密鍵の両方を指定する必要があります。",
mysqlTlsConnectionFailureHint: "MySQLサーバーがTLSを要求しない場合は、接続を編集し、TLSモードを無効にしてから再試行してください。",
mysqlClientCertBrowse: "クライアント証明書を選択",
mysqlClientKeyBrowse: "クライアント秘密鍵を選択",
postgresSslMode: "TLSモード",

View File

@ -217,6 +217,7 @@ export default withEnglishFallback({
mysqlClientCertPlaceholder: "/caminho/para/client.crt",
mysqlClientKeyPlaceholder: "/caminho/para/client.key",
mysqlClientCertHint: "O certificado e a chave privada do cliente devem ser fornecidos juntos quando o MySQL exige mTLS.",
mysqlTlsConnectionFailureHint: "Se o servidor MySQL não exigir TLS, edite a conexão, defina o Modo TLS como Desabilitado e tente novamente.",
mysqlClientCertBrowse: "Escolher certificado do cliente",
mysqlClientKeyBrowse: "Escolher chave privada do cliente",
postgresSslMode: "Modo TLS",

View File

@ -217,6 +217,7 @@ export default withEnglishFallback({
mysqlClientCertPlaceholder: "/path/to/client.crt",
mysqlClientKeyPlaceholder: "/path/to/client.key",
mysqlClientCertHint: "MySQL 要求 mTLS 时,客户端证书和私钥必须一起填写。",
mysqlTlsConnectionFailureHint: "如果 MySQL 服务端不要求 TLS请编辑连接在 TLS 模式中选择“禁用”后重试。",
mysqlClientCertBrowse: "选择客户端证书",
mysqlClientKeyBrowse: "选择客户端私钥",
postgresSslMode: "TLS 模式",

View File

@ -217,6 +217,7 @@ export default withEnglishFallback({
mysqlClientCertPlaceholder: "/path/to/client.crt",
mysqlClientKeyPlaceholder: "/path/to/client.key",
mysqlClientCertHint: "MySQL 要求 mTLS 時,用戶端憑證和私鑰必須一起填寫。",
mysqlTlsConnectionFailureHint: "如果 MySQL 伺服器不要求 TLS請編輯連線在 TLS 模式中選擇「停用」後重試。",
mysqlClientCertBrowse: "選擇用戶端憑證",
mysqlClientKeyBrowse: "選擇用戶端私鑰",
postgresSslMode: "TLS 模式",

View File

@ -0,0 +1,48 @@
import { describe, expect, it } from "vitest";
import { appendConnectionErrorHints } from "@/lib/connection/connectionErrorHints";
import type { ConnectionConfig } from "@/types/database";
function mysqlConfig(urlParams: string | undefined): ConnectionConfig {
return {
id: "mysql-test",
name: "MySQL",
db_type: "mysql",
host: "127.0.0.1",
port: 3306,
username: "root",
password: "",
database: undefined,
url_params: urlParams,
ssl: false,
};
}
const t = (key: string) => (key === "connection.mysqlTlsConnectionFailureHint" ? "Set TLS Mode to Disabled." : key);
describe("appendConnectionErrorHints", () => {
it("adds a MySQL TLS hint for non-disabled TLS failures", () => {
const message = appendConnectionErrorHints(mysqlConfig("ssl-mode=preferred"), "MySQL connection failed: TLS handshake failed", t);
expect(message).toContain("TLS handshake failed");
expect(message).toContain("Set TLS Mode to Disabled.");
});
it("adds the TLS hint for camel-case MySQL sslMode params", () => {
const message = appendConnectionErrorHints(mysqlConfig("sslMode=REQUIRED"), "MySQL connection failed: Driver error: `Client asked for SSL but server does not have this capability'", t);
expect(message).toContain("server does not have this capability");
expect(message).toContain("Set TLS Mode to Disabled.");
});
it("does not add the TLS hint when MySQL TLS is disabled", () => {
const message = appendConnectionErrorHints(mysqlConfig("ssl-mode=disabled"), "MySQL connection failed: TLS handshake failed", t);
expect(message).toBe("MySQL connection failed: TLS handshake failed");
});
it("does not add the TLS hint for non-TLS errors", () => {
const message = appendConnectionErrorHints(mysqlConfig("ssl-mode=preferred"), "Access denied for user root", t);
expect(message).toBe("Access denied for user root");
});
});

View File

@ -0,0 +1,46 @@
import type { ConnectionConfig } from "@/types/database";
type Translate = (key: string) => string;
function normalizeUrlParams(params: string | undefined): URLSearchParams {
return new URLSearchParams((params || "").trim().replace(/^\?/, ""));
}
function normalizeUrlParamKey(key: string): string {
return key.trim().toLowerCase().replace(/[-_]/g, "");
}
function urlParamValue(params: URLSearchParams, key: string): string {
const normalizedKey = normalizeUrlParamKey(key);
for (const [paramKey, value] of params.entries()) {
if (normalizeUrlParamKey(paramKey) === normalizedKey) return value;
}
return "";
}
function mysqlTlsMode(config: ConnectionConfig): string {
const parsed = normalizeUrlParams(config.url_params);
// MySQL clients use ssl-mode, sslmode and sslMode spellings; keep hint behavior aligned with backend parsing.
const mode = urlParamValue(parsed, "ssl-mode").trim().toLowerCase().replace("-", "_");
if (["disabled", "disable"].includes(mode)) return "disabled";
if (["preferred", "prefer"].includes(mode)) return "preferred";
if (["required", "require", "verify_ca", "verify_identity"].includes(mode)) return mode;
if (config.ssl || ["true", "1", "yes", "on"].includes(urlParamValue(parsed, "require_ssl").trim().toLowerCase())) return "required";
return "disabled";
}
function isMysqlTlsLikeFailure(message: string): boolean {
const text = message.toLowerCase();
return (
(text.includes("mysql") || text.includes("mariadb") || text.includes("tidb") || text.includes("tls") || text.includes("ssl")) &&
(text.includes("tls") || text.includes("ssl") || text.includes("handshake") || text.includes("certificate") || text.includes("cert") || text.includes("unknown ca") || text.includes("self signed"))
);
}
export function appendConnectionErrorHints(config: ConnectionConfig | undefined, message: string, t: Translate): string {
if (!config || config.db_type !== "mysql") return message;
if (mysqlTlsMode(config) === "disabled") return message;
if (!isMysqlTlsLikeFailure(message)) return message;
const hint = t("connection.mysqlTlsConnectionFailureHint");
return message.includes(hint) ? message : `${message}\n\n${hint}`;
}

View File

@ -60,6 +60,7 @@ import { completionSchemasFromTree, completionTablesFromTree } from "@/lib/metad
import { kvRootNodeLabel } from "@/lib/kv/kvRootPresentation";
import { REDIS_SCAN_PAGE_SIZE_DEFAULT } from "@/lib/redis/redisKeyPattern";
import { appendAgentDriverUpdateHint, hasAgentDriverUpdate, type AgentDriverInstallState } from "@/lib/connection/agentDriverInstallHint";
import { appendConnectionErrorHints } from "@/lib/connection/connectionErrorHints";
import { createMetadataLoadTrace, logMetadataLoadTrace, MetadataLoadCoordinator, type MetadataLoadTraceLogger } from "@/lib/metadata/metadataLoadCoordinator";
import type { MetadataScopeInput } from "@/lib/metadata/metadataLoadScope";
import { MetadataResultCache, type MetadataCacheInvalidation } from "@/lib/metadata/metadataResultCache";
@ -522,6 +523,7 @@ export const useConnectionStore = defineStore("connection", () => {
function connectionErrorWithDriverUpdateHint(config: ConnectionConfig | undefined, message: string): string {
if (!config) return message;
message = appendConnectionErrorHints(config, message, i18n.global.t);
if (!hasAgentDriverUpdate(config.db_type, agentDrivers.value, config.driver_profile)) return message;
return appendAgentDriverUpdateHint(message, agentDriverUpdateHint());
}