Skip to content

Commit fc4e772

Browse files
sfc-gh-pczajkasfc-gh-turbaszek
authored andcommitted
Snow 1747564 econnreset error should be retried (#2547)
1 parent 1a20b32 commit fc4e772

File tree

5 files changed

+133
-1
lines changed

5 files changed

+133
-1
lines changed

DESCRIPTION.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ Source code is also available at: https://github.com/snowflakedb/snowflake-conne
1010
- v3.18.0(TBD)
1111
- Added the `workload_identity_impersonation_path` parameter to support service account impersonation for Workload Identity Federation on GCP and AWS workloads only
1212
- Fixed `get_results_from_sfqid` when using `DictCursor` and executing multiple statements at once
13+
- Added the `oauth_credentials_in_body` parameter supporting an option to send the oauth client credentials in the request body
14+
- Fix retry behavior for `ECONNRESET` error
15+
16+
- v3.17.4(September 22,2025)
17+
- Added support for intermediate certificates as roots when they are stored in the trust store
18+
- Bumped up vendored `urllib3` to `2.5.0` and `requests` to `v2.32.5`
19+
- Dropped support for OpenSSL versions older than 1.1.1
1320

1421
- v3.17.3(September 02,2025)
1522
- Enhanced configuration file permission warning messages.

src/snowflake/connector/aio/_network.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
from ..network import (
6868
SnowflakeRestfulJsonEncoder,
6969
get_http_retryable_error,
70+
is_econnreset_exception,
7071
is_login_request,
7172
is_retryable_http_code,
7273
)
@@ -790,6 +791,8 @@ async def _request_exec(
790791
finally:
791792
raw_ret.close() # ensure response is closed
792793
except (aiohttp.ClientSSLError, aiohttp.ClientConnectorSSLError) as se:
794+
if is_econnreset_exception(se):
795+
raise RetryRequest(se.os_error)
793796
msg = f"Hit non-retryable SSL error, {str(se)}.\n{_CONNECTIVITY_ERR_MSG}"
794797
logger.debug(msg)
795798
# the following code is for backward compatibility with old versions of python connector which calls

src/snowflake/connector/network.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,10 @@ def is_login_request(url: str) -> bool:
238238
return "login-request" in parse_url(url).path
239239

240240

241+
def is_econnreset_exception(e: Exception) -> bool:
242+
return "ECONNRESET" in repr(e)
243+
244+
241245
class RetryRequest(Exception):
242246
"""Signal to retry request."""
243247

@@ -965,7 +969,7 @@ def _request_exec_wrapper(
965969
)
966970
retry_ctx.retry_reason = reason
967971

968-
if "Connection aborted" in repr(e) and "ECONNRESET" in repr(e):
972+
if is_econnreset_exception(e):
969973
# connection is reset by the server, the underlying connection is broken and can not be reused
970974
# we need a new urllib3 http(s) connection in this case.
971975
# We need to first close the old one so that urllib3 pool manager can create a new connection
@@ -1146,6 +1150,8 @@ def _request_exec(
11461150
finally:
11471151
raw_ret.close() # ensure response is closed
11481152
except SSLError as se:
1153+
if is_econnreset_exception(se):
1154+
raise RetryRequest(se)
11491155
msg = f"Hit non-retryable SSL error, {str(se)}.\n{_CONNECTIVITY_ERR_MSG}"
11501156
logger.debug(msg)
11511157
# the following code is for backward compatibility with old versions of python connector which calls

test/unit/aio/test_retry_network_async.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import aiohttp
1919
import OpenSSL.SSL
2020
import pytest
21+
from aiohttp import ClientSSLError
2122

2223
import snowflake.connector.aio
2324
from snowflake.connector.aio._network import SnowflakeRestful
@@ -41,6 +42,7 @@
4142
ServiceUnavailableError,
4243
)
4344
from snowflake.connector.network import STATUS_TO_EXCEPTION, RetryRequest
45+
from snowflake.connector.vendored.requests.exceptions import SSLError
4446

4547
pytestmark = pytest.mark.skipolddriver
4648

@@ -454,3 +456,60 @@ async def test_retry_request_timeout(mockSessionRequest, next_action_result):
454456
# 13 seconds should be enough for authenticator to attempt thrice
455457
# however, loosen restrictions to avoid thread scheduling causing failure
456458
assert 1 < mockSessionRequest.call_count < 5
459+
460+
461+
async def test_sslerror_with_econnreset_retries():
462+
"""Test that SSLError with ECONNRESET raises RetryRequest."""
463+
connection = mock_connection()
464+
connection.errorhandler = Error.default_errorhandler
465+
rest = SnowflakeRestful(
466+
host="testaccount.snowflakecomputing.com",
467+
port=443,
468+
connection=connection,
469+
)
470+
471+
default_parameters = {
472+
"method": "POST",
473+
"full_url": "https://testaccount.snowflakecomputing.com/",
474+
"headers": {},
475+
"data": '{"code": 12345}',
476+
"token": None,
477+
}
478+
479+
# Test SSLError with ECONNRESET in the message
480+
econnreset_ssl_error = ClientSSLError(
481+
MagicMock(), SSLError("Connection broken: ECONNRESET")
482+
)
483+
session = MagicMock()
484+
session.request = Mock(side_effect=econnreset_ssl_error)
485+
486+
with pytest.raises(RetryRequest, match="Connection broken: ECONNRESET"):
487+
await rest._request_exec(session=session, **default_parameters)
488+
489+
490+
async def test_sslerror_without_econnreset_does_not_retry():
491+
"""Test that SSLError without ECONNRESET does not retry but raises OperationalError."""
492+
connection = mock_connection()
493+
connection.errorhandler = Error.default_errorhandler
494+
rest = SnowflakeRestful(
495+
host="testaccount.snowflakecomputing.com",
496+
port=443,
497+
connection=connection,
498+
)
499+
500+
default_parameters = {
501+
"method": "POST",
502+
"full_url": "https://testaccount.snowflakecomputing.com/",
503+
"headers": {},
504+
"data": '{"code": 12345}',
505+
"token": None,
506+
}
507+
508+
# Test SSLError without ECONNRESET in the message
509+
regular_ssl_error = SSLError("SSL handshake failed")
510+
session = MagicMock()
511+
session.request = Mock(side_effect=regular_ssl_error)
512+
513+
# This should raise OperationalError, not RetryRequest
514+
with pytest.raises(OperationalError):
515+
await rest._request_exec(session=session, **default_parameters)

test/unit/test_retry_network.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,11 @@
5151
try:
5252
import snowflake.connector.vendored.urllib3.contrib.pyopenssl
5353
from snowflake.connector.vendored import requests, urllib3
54+
from snowflake.connector.vendored.requests.exceptions import SSLError
5455
except ImportError: # pragma: no cover
5556
import requests
5657
import urllib3
58+
from requests.exceptions import SSLError
5759

5860
THIS_DIR = os.path.dirname(os.path.realpath(__file__))
5961

@@ -477,3 +479,58 @@ def test_retry_request_timeout(mockSessionRequest, next_action_result):
477479
# 13 seconds should be enough for authenticator to attempt thrice
478480
# however, loosen restrictions to avoid thread scheduling causing failure
479481
assert 1 < mockSessionRequest.call_count < 5
482+
483+
484+
def test_sslerror_with_econnreset_retries():
485+
"""Test that SSLError with ECONNRESET raises RetryRequest."""
486+
connection = mock_connection()
487+
connection.errorhandler = Error.default_errorhandler
488+
rest = SnowflakeRestful(
489+
host="testaccount.snowflakecomputing.com",
490+
port=443,
491+
connection=connection,
492+
)
493+
494+
default_parameters = {
495+
"method": "POST",
496+
"full_url": "https://testaccount.snowflakecomputing.com/",
497+
"headers": {},
498+
"data": '{"code": 12345}',
499+
"token": None,
500+
}
501+
502+
# Test SSLError with ECONNRESET in the message
503+
econnreset_ssl_error = SSLError("Connection broken: ECONNRESET")
504+
session = MagicMock()
505+
session.request = Mock(side_effect=econnreset_ssl_error)
506+
507+
with pytest.raises(RetryRequest, match="Connection broken: ECONNRESET"):
508+
rest._request_exec(session=session, **default_parameters)
509+
510+
511+
def test_sslerror_without_econnreset_does_not_retry():
512+
"""Test that SSLError without ECONNRESET does not retry but raises OperationalError."""
513+
connection = mock_connection()
514+
connection.errorhandler = Error.default_errorhandler
515+
rest = SnowflakeRestful(
516+
host="testaccount.snowflakecomputing.com",
517+
port=443,
518+
connection=connection,
519+
)
520+
521+
default_parameters = {
522+
"method": "POST",
523+
"full_url": "https://testaccount.snowflakecomputing.com/",
524+
"headers": {},
525+
"data": '{"code": 12345}',
526+
"token": None,
527+
}
528+
529+
# Test SSLError without ECONNRESET in the message
530+
regular_ssl_error = SSLError("SSL handshake failed")
531+
session = MagicMock()
532+
session.request = Mock(side_effect=regular_ssl_error)
533+
534+
# This should raise OperationalError, not RetryRequest
535+
with pytest.raises(OperationalError):
536+
rest._request_exec(session=session, **default_parameters)

0 commit comments

Comments
 (0)