Skip to content

Commit 73910ce

Browse files
SNOW-2866221: Retry transient SSLHandshakeException errors caused by EOFException (#2423)
1 parent b8f30ae commit 73910ce

3 files changed

Lines changed: 58 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- Bumped grpc-java to 1.77.0 to address CVE-2025-58057 from transient dep (snowflakedb/snowflake-jdbc#2415)
77
- Fix Connection and socket timeout are now propagated to HTTP client.
88
- Fix Azure 503 retries and configure it with the putGetMaxRetries parameter.
9+
- Improved retries for SSLHandshakeException errors caused by transient EOFException
910

1011
- v3.27.1
1112
- Added platform detection on login to set PLATFORM metric in CLIENT_ENVIRONMENT

src/main/java/net/snowflake/client/jdbc/RestRequest.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.fasterxml.jackson.databind.JsonNode;
66
import com.fasterxml.jackson.databind.ObjectMapper;
77
import com.fasterxml.jackson.databind.node.ObjectNode;
8+
import java.io.EOFException;
89
import java.io.IOException;
910
import java.io.InputStream;
1011
import java.io.PrintWriter;
@@ -651,6 +652,11 @@ private static Throwable getRootCause(Throwable ex) {
651652
return ex0;
652653
}
653654

655+
private static boolean isTransientHandshakeEOF(Exception ex) {
656+
Throwable root = getRootCause(ex);
657+
return root instanceof EOFException;
658+
}
659+
654660
private static void setRequestConfig(
655661
HttpRequestBase httpRequest,
656662
boolean withoutCookies,
@@ -979,6 +985,10 @@ public static HttpResponseContextDto executeWithRetries(
979985
httpClient = HttpUtil.getHttpClient(key, httpHeaderCustomizer);
980986
}
981987
continue;
988+
} else if (ex instanceof SSLHandshakeException && isTransientHandshakeEOF(ex)) {
989+
// Treat transient EOF during TLS handshake as retryable:
990+
// set saved exception for logging/telemetry and allow the retry loop to proceed.
991+
responseDto.setSavedEx(ex);
982992
} else {
983993
responseDto.setSavedEx(handlingNotRetryableException(ex, httpExecutingContext));
984994
}

src/test/java/net/snowflake/client/jdbc/RestRequestTest.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import com.fasterxml.jackson.databind.node.ObjectNode;
2222
import java.io.ByteArrayInputStream;
23+
import java.io.EOFException;
2324
import java.io.IOException;
2425
import java.io.InputStream;
2526
import java.net.SocketException;
@@ -397,6 +398,52 @@ class TestCase {
397398
}
398399
}
399400

401+
@Test
402+
public void testRetryOnTransientSslHandshakeEof() throws Exception {
403+
CloseableHttpClient client = mock(CloseableHttpClient.class);
404+
// First attempt throws SSLHandshakeException with EOF root cause, second succeeds
405+
SSLHandshakeException handshake = new SSLHandshakeException("handshake failed");
406+
handshake.initCause(new EOFException("remote host closed connection during handshake"));
407+
when(client.execute(any(HttpUriRequest.class)))
408+
.thenThrow(handshake)
409+
.thenReturn(successResponse());
410+
411+
// Use helper to execute with maxRetries >= 1 so the loop can retry
412+
execute(
413+
client,
414+
"fakeurl.com/?requestId=abcd-1234",
415+
/* retryTimeout */ 0,
416+
/* authTimeout */ 0,
417+
/* socketTimeout */ 0,
418+
/* includeRetryParameters */ true,
419+
/* noRetry */ false,
420+
/* maxRetries */ 2);
421+
422+
// Verify two executions (first throws, second succeeds)
423+
verify(client, times(2)).execute(any(HttpUriRequest.class));
424+
}
425+
426+
@Test
427+
public void testNoRetryOnSslHandshakeWithoutEof() throws Exception {
428+
CloseableHttpClient client = mock(CloseableHttpClient.class);
429+
// SSLHandshakeException without EOF root cause should be treated as non-retryable
430+
SSLHandshakeException handshake = new SSLHandshakeException("handshake failed (non-EOF)");
431+
when(client.execute(any(HttpUriRequest.class))).thenThrow(handshake);
432+
433+
assertThrows(
434+
SnowflakeSQLException.class,
435+
() ->
436+
execute(
437+
client,
438+
"fakeurl.com/?requestId=abcd-1234",
439+
/* retryTimeout */ 0,
440+
/* authTimeout */ 0,
441+
/* socketTimeout */ 0,
442+
/* includeRetryParameters */ true,
443+
/* noRetry */ false,
444+
/* maxRetries */ 2));
445+
}
446+
400447
@Test
401448
public void testExceptionAuthBasedTimeout() throws IOException {
402449
CloseableHttpClient client = mock(CloseableHttpClient.class);

0 commit comments

Comments
 (0)