Skip to content

Commit ace34bd

Browse files
SNOW-2439940: Deprecated custom revocation error classes in OCSP (#2748)
1 parent a4a43b5 commit ace34bd

4 files changed

Lines changed: 84 additions & 13 deletions

File tree

DESCRIPTION.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Source code is also available at: https://github.com/snowflakedb/snowflake-conne
1212
- Made the parameter `server_session_keep_alive` in `SnowflakeConnection` skip checking for pending async queries, providing faster connection close times especially when many async queries are executed.
1313
- Fix string representation of INTERVAL YEAR and INTERVAL MONTH types.
1414
- Log a warning when using http protocol for OAuth urls.
15+
- Deprecated support for custom revocation error classes in OCSP response cache deserialization. By default, only `RevocationCheckError` exceptions are deserialized from OCSP cache. Custom exception classes can be temporarily enabled by setting the `SNOWFLAKE_ENABLE_CUSTOM_REVOCATION_ERRORS` environment variable to `true` or `1`, but this support will be removed in a future release.
1516

1617
- v4.2.0(January 07,2026)
1718
- Added `SnowflakeCursor.stats` property to expose granular DML statistics (rows inserted, deleted, updated, and duplicates) for operations like CTAS where `rowcount` is insufficient.

src/snowflake/connector/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,7 @@ class IterUnit(Enum):
442442
ENV_VAR_PARTNER = "SF_PARTNER"
443443
ENV_VAR_TEST_MODE = "SNOWFLAKE_TEST_MODE"
444444
ENV_VAR_DISABLE_PLATFORM_DETECTION = "SNOWFLAKE_DISABLE_PLATFORM_DETECTION"
445+
ENV_VAR_ENABLE_CUSTOM_REVOCATION_ERRORS = "SNOWFLAKE_ENABLE_CUSTOM_REVOCATION_ERRORS"
445446

446447
# Boolean positive values (lowercased) for environment variable checks
447448
ENV_VAR_BOOL_POSITIVE_VALUES_LOWERCASED = ["true"]

src/snowflake/connector/ocsp_snowflake.py

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import sys
1111
import tempfile
1212
import time
13+
import warnings
1314
from base64 import b64decode, b64encode
1415
from datetime import datetime, timezone
1516
from logging import getLogger
@@ -28,7 +29,10 @@
2829

2930
from snowflake.connector import SNOWFLAKE_CONNECTOR_VERSION
3031
from snowflake.connector.compat import OK, urlsplit, urlunparse
31-
from snowflake.connector.constants import HTTP_HEADER_USER_AGENT
32+
from snowflake.connector.constants import (
33+
ENV_VAR_ENABLE_CUSTOM_REVOCATION_ERRORS,
34+
HTTP_HEADER_USER_AGENT,
35+
)
3236
from snowflake.connector.errorcode import (
3337
ER_INVALID_OCSP_RESPONSE_SSD,
3438
ER_INVALID_SSD,
@@ -122,19 +126,40 @@ def deserialize_exception(exception_dict: dict | None) -> Exception | None:
122126
return
123127
exc_class = exception_dict.get("class")
124128
exc_module = exception_dict.get("module")
129+
130+
# For RevocationCheckError, deserialize directly
131+
if (
132+
exc_class == "RevocationCheckError"
133+
and exc_module == "snowflake.connector.errors"
134+
):
135+
return RevocationCheckError(
136+
msg=exception_dict["msg"],
137+
errno=exception_dict.get("errno", ER_OCSP_RESPONSE_LOAD_FAILURE),
138+
)
139+
140+
# For non-RevocationCheckError, always try to deserialize to detect failures
125141
try:
126-
if (
127-
exc_class == "RevocationCheckError"
128-
and exc_module == "snowflake.connector.errors"
142+
module = importlib.import_module(exc_module)
143+
exc_cls = getattr(module, exc_class)
144+
custom_exc = exc_cls(exception_dict["msg"])
145+
146+
# If env var is set, return the custom exception with deprecation warning
147+
if os.getenv(ENV_VAR_ENABLE_CUSTOM_REVOCATION_ERRORS, "").lower() in (
148+
"true",
149+
"1",
129150
):
130-
return RevocationCheckError(
131-
msg=exception_dict["msg"],
132-
errno=exception_dict["errno"],
151+
warnings.warn(
152+
"Support for custom revocation error classes will be removed in a future release.",
153+
DeprecationWarning,
154+
stacklevel=3,
133155
)
134-
else:
135-
module = importlib.import_module(exc_module)
136-
exc_cls = getattr(module, exc_class)
137-
return exc_cls(exception_dict["msg"])
156+
return custom_exc
157+
158+
# Otherwise, convert to RevocationCheckError
159+
return RevocationCheckError(
160+
msg=exception_dict["msg"],
161+
errno=exception_dict.get("errno", ER_OCSP_RESPONSE_LOAD_FAILURE),
162+
)
138163
except Exception as deserialize_exc:
139164
logger.debug(
140165
f"hitting error {str(deserialize_exc)} while deserializing exception,"

test/unit/test_ocsp.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -862,7 +862,8 @@ def verify_none(origin_cache, loaded_cache):
862862
):
863863
assert key1 == key2 and value1 == value2
864864

865-
def verify_exception(_, loaded_cache):
865+
def verify_exception_default(_, loaded_cache):
866+
"""Test default behavior: custom exceptions are converted to RevocationCheckError"""
866867
exc_1 = loaded_cache[(b"key1", b"key2", b"key3")].exception
867868
exc_2 = loaded_cache[(b"key4", b"key5", b"key6")].exception
868869
exc_3 = loaded_cache[(b"key7", b"key8", b"key9")].exception
@@ -871,6 +872,25 @@ def verify_exception(_, loaded_cache):
871872
and exc_1.raw_msg == "error"
872873
and exc_1.errno == 1
873874
)
875+
# By default, custom exceptions (ValueError) are converted to RevocationCheckError
876+
assert isinstance(exc_2, RevocationCheckError)
877+
assert (
878+
isinstance(exc_3, RevocationCheckError)
879+
and "while deserializing ocsp cache, please try cleaning up the OCSP cache under directory"
880+
in exc_3.msg
881+
)
882+
883+
def verify_exception_deprecated(_, loaded_cache):
884+
"""Test deprecated behavior with env var: custom exceptions are preserved"""
885+
exc_1 = loaded_cache[(b"key1", b"key2", b"key3")].exception
886+
exc_2 = loaded_cache[(b"key4", b"key5", b"key6")].exception
887+
exc_3 = loaded_cache[(b"key7", b"key8", b"key9")].exception
888+
assert (
889+
isinstance(exc_1, RevocationCheckError)
890+
and exc_1.raw_msg == "error"
891+
and exc_1.errno == 1
892+
)
893+
# With env var set, custom exceptions are preserved
874894
assert isinstance(exc_2, ValueError) and str(exc_2) == "value error"
875895
assert (
876896
isinstance(exc_3, RevocationCheckError)
@@ -886,6 +906,7 @@ def verify_exception(_, loaded_cache):
886906
)
887907
verify(verify_none, origin_cache)
888908

909+
# Test default behavior (no env var)
889910
origin_cache = copy.deepcopy(test_cache)
890911
origin_cache.update(
891912
{
@@ -900,4 +921,27 @@ def verify_exception(_, loaded_cache):
900921
),
901922
}
902923
)
903-
verify(verify_exception, origin_cache)
924+
verify(verify_exception_default, origin_cache)
925+
926+
# Test deprecated behavior (with env var set)
927+
with mock.patch.dict(
928+
os.environ, {"SNOWFLAKE_ENABLE_CUSTOM_REVOCATION_ERRORS": "true"}
929+
):
930+
origin_cache = copy.deepcopy(test_cache)
931+
origin_cache.update(
932+
{
933+
(b"key1", b"key2", b"key3"): OCSPResponseValidationResult(
934+
exception=RevocationCheckError(msg="error", errno=1),
935+
),
936+
(b"key4", b"key5", b"key6"): OCSPResponseValidationResult(
937+
exception=ValueError("value error"),
938+
),
939+
(b"key7", b"key8", b"key9"): OCSPResponseValidationResult(
940+
exception=json.JSONDecodeError("json error", "doc", 0)
941+
),
942+
}
943+
)
944+
with pytest.warns(
945+
DeprecationWarning, match="Support for custom revocation error classes"
946+
):
947+
verify(verify_exception_deprecated, origin_cache)

0 commit comments

Comments
 (0)