Skip to content

Commit 5a798d4

Browse files
authored
enhanced ssl certificate error message (#914)
## Description <!-- Provide a brief summary of the changes made and the issue they aim to address.--> This PR implements improved SSL handshake error messages for certificate path failures. Previously, users received error messages like "Error while establishing a connection in databricks" or "PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target" when encountering SSL certificate validation issues. The newer implementation provides suggestions to users with actionable guidance for resolving SSL certificate issues as shown below. ``` Unable to find certification path to requested target in truststore: /path/to/truststore.jks SSL Error: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target Details: TLS handshake failure due to TLS Certificate of server being connected is not in the configured truststore. Next steps: - Make sure that the connection string has the appropriate Databricks workspace FQDN. - Verify the configured truststore path and make sure the required certificates are imported. . PEM certificate chain of the warehouse endpoint can be fetched using "openssl s_client -connect <workspace>:443 -showcerts" . Reference KB article with troubleshooting steps. ``` ## Testing <!-- Describe how the changes have been tested--> ### Unit Tests: - **`testCreateSessionWithSSLCertificatePathError()`** - Tests SSL error detection using Mockito to mock `DatabricksError` with `SSLHandshakeException` cause - **`testCreateSessionWithNonSSLError()`** - Ensures non-SSL errors still return generic messages - All existing tests continue to pass ## Additional Notes to the Reviewer <!-- Share any additional context or insights that may help the reviewer understand the changes better. This could include challenges faced, limitations, or compromises made during the development process. Also, mention any areas of the code that you would like the reviewer to focus on specifically. --> --------- Signed-off-by: Sreekanth Vadigi <[email protected]>
1 parent 5f89527 commit 5a798d4

File tree

4 files changed

+141
-2
lines changed

4 files changed

+141
-2
lines changed

NEXT_CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
### Updated
1515
- Column name support for JDBC ResultSet operations is now case-insensitive
16+
- Enhanced SSL certificate path validation error messages to provide actionable troubleshooting steps.
1617

1718
### Fixed
1819
- Fixed Bouncy Castle registration conflicts by using local provider instance instead of global security registration.

src/main/java/com/databricks/jdbc/dbclient/impl/sqlexec/DatabricksSdkClient.java

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,13 @@
4242
import com.google.common.annotations.VisibleForTesting;
4343
import java.io.IOException;
4444
import java.math.BigDecimal;
45+
import java.net.URL;
4546
import java.sql.SQLException;
4647
import java.time.Instant;
4748
import java.util.*;
4849
import java.util.stream.Collectors;
50+
import java.util.stream.Stream;
51+
import javax.net.ssl.SSLHandshakeException;
4952

5053
/** Implementation of IDatabricksClient interface using Databricks Java SDK. */
5154
public class DatabricksSdkClient implements IDatabricksClient {
@@ -121,7 +124,7 @@ public ImmutableSessionInfo createSession(
121124
if (e.getStatusCode() == TEMPORARY_REDIRECT_STATUS_CODE) {
122125
throw new DatabricksTemporaryRedirectException(TEMPORARY_REDIRECT_EXCEPTION);
123126
}
124-
String errorReason = "Error while establishing a connection in databricks";
127+
String errorReason = buildErrorMessage(e);
125128
throw new DatabricksSQLException(errorReason, e, DatabricksDriverErrorCode.CONNECTION_ERROR);
126129
} catch (IOException e) {
127130
String errorMessage = "Error while processing the request via the sdk client";
@@ -549,4 +552,56 @@ private ExecuteStatementResponse wrapGetStatementResponse(
549552
.setManifest(getStatementResponse.getManifest())
550553
.setResult(getStatementResponse.getResult());
551554
}
555+
556+
/**
557+
* Builds actionable error messages for SSL handshake failures. Returns a generic message if the
558+
* error is not SSL-related.
559+
*/
560+
private String buildErrorMessage(DatabricksError e) {
561+
562+
boolean isSSLException =
563+
Stream.iterate(e.getCause(), Objects::nonNull, Throwable::getCause)
564+
.anyMatch(cause -> cause instanceof SSLHandshakeException);
565+
566+
boolean isCertificatePathError =
567+
e.getMessage().contains("PKIX path building failed")
568+
|| e.getMessage().contains("unable to find valid certification path");
569+
570+
if (isSSLException && isCertificatePathError) {
571+
return buildSSLCertificatePathErrorMessage(e);
572+
}
573+
574+
return "Error while establishing a connection in databricks";
575+
}
576+
577+
/** Builds the SSL certificate path error message with actionable steps. */
578+
private String buildSSLCertificatePathErrorMessage(DatabricksError e) {
579+
580+
String customTruststorePathMessage = "";
581+
if (connectionContext != null && connectionContext.getSSLTrustStore() != null) {
582+
customTruststorePathMessage = " in truststore: " + connectionContext.getSSLTrustStore();
583+
}
584+
585+
// Get the actual workspace hostname for the openssl command
586+
// by removing protocol and port from host url
587+
String workspaceHostname = "<workspace>";
588+
try {
589+
if (connectionContext != null && connectionContext.getHostUrl() != null) {
590+
workspaceHostname = new URL(connectionContext.getHostUrl()).getHost();
591+
}
592+
} catch (Exception ex) {
593+
LOGGER.debug("Could not retrieve workspace hostname for error message", ex);
594+
}
595+
596+
return String.format(
597+
"Unable to find certification path to requested target%s\n\n"
598+
+ "SSL Error: %s\n\n"
599+
+ "Details: TLS handshake failure due to TLS Certificate of server being connected is not in the configured truststore.\n\n"
600+
+ "Next steps:\n"
601+
+ "- Make sure that the connection string has the appropriate Databricks workspace FQDN.\n\n"
602+
+ "- Verify the configured truststore path and make sure the required certificates are imported.\n"
603+
+ " . PEM certificate chain of the warehouse endpoint can be fetched using \"openssl s_client -connect %s:443 -showcerts\"\n"
604+
+ " . Reference KB article with troubleshooting steps.\n",
605+
customTruststorePathMessage, e.getMessage(), workspaceHostname);
606+
}
552607
}

src/test/java/com/databricks/jdbc/dbclient/impl/common/ConfiguratorUtilsTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ private static void createEmptyStore(String filePath, String storeType, String p
9696
* @param alias The alias for the certificate entry
9797
* @param isKeyStore Whether this is a keystore (with private key) or truststore (cert only)
9898
*/
99-
private static void createDummyStore(
99+
public static void createDummyStore(
100100
String filePath, String storeType, String password, String alias, boolean isKeyStore)
101101
throws Exception {
102102
KeyStore keyStore = KeyStore.getInstance(storeType);

src/test/java/com/databricks/jdbc/dbclient/impl/sqlexec/DatabricksSdkClientTest.java

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@
99
import static org.junit.jupiter.api.Assertions.*;
1010
import static org.mockito.ArgumentMatchers.any;
1111
import static org.mockito.Mockito.*;
12+
import static org.mockito.Mockito.mock;
13+
import static org.mockito.Mockito.when;
1214

1315
import com.databricks.jdbc.api.impl.*;
1416
import com.databricks.jdbc.api.internal.IDatabricksConnectionContext;
1517
import com.databricks.jdbc.common.IDatabricksComputeResource;
1618
import com.databricks.jdbc.common.StatementType;
1719
import com.databricks.jdbc.common.Warehouse;
1820
import com.databricks.jdbc.common.util.DatabricksTypeUtil;
21+
import com.databricks.jdbc.dbclient.impl.common.ConfiguratorUtilsTest;
1922
import com.databricks.jdbc.dbclient.impl.common.StatementId;
2023
import com.databricks.jdbc.exception.DatabricksSQLException;
2124
import com.databricks.jdbc.exception.DatabricksTemporaryRedirectException;
@@ -31,9 +34,11 @@
3134
import com.databricks.sdk.core.DatabricksError;
3235
import com.databricks.sdk.core.http.Request;
3336
import com.databricks.sdk.service.sql.*;
37+
import java.io.File;
3438
import java.io.IOException;
3539
import java.math.BigDecimal;
3640
import java.util.*;
41+
import javax.net.ssl.SSLHandshakeException;
3742
import org.junit.jupiter.api.Test;
3843
import org.junit.jupiter.api.extension.ExtendWith;
3944
import org.mockito.Mock;
@@ -52,6 +57,8 @@ public class DatabricksSdkClientTest {
5257
"SELECT * FROM orders WHERE user_id = ? AND shard = ? AND region_code = ? AND namespace = ?";
5358
private static final String JDBC_URL =
5459
"jdbc:databricks://sample-host.18.azuredatabricks.net:4423/default;transportMode=http;ssl=1;AuthMech=3;httpPath=/sql/1.0/warehouses/99999999;";
60+
private static final String DEFAULT_KEYSTORE_PASSWORD = "changeit";
61+
5562
private static final Map<String, String> headers =
5663
new HashMap<>() {
5764
{
@@ -497,6 +504,82 @@ public void testNullValue() throws DatabricksSQLException {
497504
assertNull(result.getValue());
498505
}
499506

507+
@Test
508+
public void testCreateSessionWithSSLCertificatePathError() throws Exception {
509+
510+
File wrongTrustStore = File.createTempFile("wrong-trust-store", ".jks");
511+
wrongTrustStore.deleteOnExit();
512+
ConfiguratorUtilsTest.createDummyStore(
513+
wrongTrustStore.getAbsolutePath(), "JKS", DEFAULT_KEYSTORE_PASSWORD, "wrong-ca", false);
514+
515+
SSLHandshakeException sslException =
516+
new SSLHandshakeException(
517+
"PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target");
518+
519+
DatabricksError sslError = mock(DatabricksError.class);
520+
when(sslError.getMessage()).thenReturn(sslException.getMessage());
521+
when(sslError.getCause()).thenReturn(sslException);
522+
523+
when(apiClient.execute(any(Request.class), eq(CreateSessionResponse.class)))
524+
.thenThrow(sslError);
525+
526+
Properties props = new Properties();
527+
props.setProperty("SSLTrustStore", wrongTrustStore.getAbsolutePath());
528+
props.setProperty("SSLTrustStorePwd", DEFAULT_KEYSTORE_PASSWORD);
529+
props.setProperty("SSLTrustStoreType", "JKS");
530+
531+
IDatabricksConnectionContext connectionContext =
532+
DatabricksConnectionContext.parse(JDBC_URL, props);
533+
DatabricksSdkClient databricksSdkClient =
534+
new DatabricksSdkClient(connectionContext, statementExecutionService, apiClient);
535+
536+
// Assert that createSession throws a DatabricksSQLException with actionable error message
537+
DatabricksSQLException exception =
538+
assertThrows(
539+
DatabricksSQLException.class,
540+
() -> databricksSdkClient.createSession(warehouse, null, null, null));
541+
542+
String errorMessage = exception.getMessage();
543+
544+
// Verify that we get the exact SSL error message
545+
String expectedErrorMessage =
546+
String.format(
547+
"Unable to find certification path to requested target in truststore: %s\n\n"
548+
+ "SSL Error: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target\n\n"
549+
+ "Details: TLS handshake failure due to TLS Certificate of server being connected is not in the configured truststore.\n\n"
550+
+ "Next steps:\n"
551+
+ "- Make sure that the connection string has the appropriate Databricks workspace FQDN.\n\n"
552+
+ "- Verify the configured truststore path and make sure the required certificates are imported.\n"
553+
+ " . PEM certificate chain of the warehouse endpoint can be fetched using \"openssl s_client -connect sample-host.18.azuredatabricks.net:443 -showcerts\"\n"
554+
+ " . Reference KB article with troubleshooting steps.\n",
555+
wrongTrustStore.getAbsolutePath());
556+
assertEquals(expectedErrorMessage, errorMessage);
557+
558+
// Clean up
559+
wrongTrustStore.delete();
560+
}
561+
562+
@Test
563+
public void testCreateSessionWithNonSSLError() throws IOException, DatabricksSQLException {
564+
565+
DatabricksError nonSSLError = new DatabricksError("500", "Some other error", 500);
566+
when(apiClient.execute(any(Request.class), eq(CreateSessionResponse.class)))
567+
.thenThrow(nonSSLError);
568+
569+
IDatabricksConnectionContext connectionContext =
570+
DatabricksConnectionContext.parse(JDBC_URL, new Properties());
571+
DatabricksSdkClient databricksSdkClient =
572+
new DatabricksSdkClient(connectionContext, statementExecutionService, apiClient);
573+
574+
DatabricksSQLException exception =
575+
assertThrows(
576+
DatabricksSQLException.class,
577+
() -> databricksSdkClient.createSession(warehouse, null, null, null));
578+
579+
String errorMessage = exception.getMessage();
580+
assertEquals("Error while establishing a connection in databricks", errorMessage);
581+
}
582+
500583
private static ImmutableSqlParameter getSqlParam(
501584
int parameterIndex, Object x, String databricksType) {
502585
return ImmutableSqlParameter.builder()

0 commit comments

Comments
 (0)