Skip to content

Commit c422eb3

Browse files
SNOW-2129434: Add in-band http exception telemetry (#2414)
1 parent bcb8c80 commit c422eb3

File tree

11 files changed

+244
-38
lines changed

11 files changed

+244
-38
lines changed

DESCRIPTION.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Source code is also available at: https://github.com/snowflakedb/snowflake-conne
99
# Release Notes
1010
- v3.16.1(TBD)
1111
- Added in-band OCSP exception telemetry.
12+
- Added in-band HTTP exception telemetry.
1213

1314
- v3.16.0(July 04,2025)
1415
- Bumped numpy dependency from <2.1.0 to <=2.2.4.

src/snowflake/connector/errorcode.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,5 @@
8989
ER_NO_PYARROW_SNOWSQL = 255004
9090
ER_FAILED_TO_READ_ARROW_STREAM = 255005
9191
ER_NO_NUMPY = 255006
92+
93+
ER_HTTP_GENERAL_ERROR = 290000

src/snowflake/connector/errors.py

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from logging import getLogger
99
from typing import TYPE_CHECKING, Any
1010

11+
from .errorcode import ER_HTTP_GENERAL_ERROR
1112
from .secret_detector import SecretDetector
1213
from .telemetry import TelemetryData, TelemetryField
1314
from .time_util import get_time_millis
@@ -374,6 +375,15 @@ class InterfaceError(Error):
374375
pass
375376

376377

378+
class HttpError(Error):
379+
def __init__(self, **kwargs) -> None:
380+
Error.__init__(
381+
self,
382+
errtype=TelemetryField.HTTP_EXCEPTION,
383+
**kwargs,
384+
)
385+
386+
377387
class DatabaseError(Error):
378388
"""Exception for errors related to the database."""
379389

@@ -439,7 +449,8 @@ def __init__(self, **kwargs) -> None:
439449
Error.__init__(
440450
self,
441451
msg=kwargs.get("msg") or "HTTP 500: Internal Server Error",
442-
errno=kwargs.get("errno"),
452+
errno=ER_HTTP_GENERAL_ERROR + kwargs.get("errno", 0),
453+
errtype=TelemetryField.HTTP_EXCEPTION,
443454
sqlstate=kwargs.get("sqlstate"),
444455
sfqid=kwargs.get("sfqid"),
445456
)
@@ -452,7 +463,8 @@ def __init__(self, **kwargs) -> None:
452463
Error.__init__(
453464
self,
454465
msg=kwargs.get("msg") or "HTTP 503: Service Unavailable",
455-
errno=kwargs.get("errno"),
466+
errno=ER_HTTP_GENERAL_ERROR + kwargs.get("errno", 0),
467+
errtype=TelemetryField.HTTP_EXCEPTION,
456468
sqlstate=kwargs.get("sqlstate"),
457469
sfqid=kwargs.get("sfqid"),
458470
)
@@ -465,7 +477,8 @@ def __init__(self, **kwargs) -> None:
465477
Error.__init__(
466478
self,
467479
msg=kwargs.get("msg") or "HTTP 504: Gateway Timeout",
468-
errno=kwargs.get("errno"),
480+
errno=ER_HTTP_GENERAL_ERROR + kwargs.get("errno", 0),
481+
errtype=TelemetryField.HTTP_EXCEPTION,
469482
sqlstate=kwargs.get("sqlstate"),
470483
sfqid=kwargs.get("sfqid"),
471484
)
@@ -478,7 +491,8 @@ def __init__(self, **kwargs) -> None:
478491
Error.__init__(
479492
self,
480493
msg=kwargs.get("msg") or "HTTP 403: Forbidden",
481-
errno=kwargs.get("errno"),
494+
errno=ER_HTTP_GENERAL_ERROR + kwargs.get("errno", 0),
495+
errtype=TelemetryField.HTTP_EXCEPTION,
482496
sqlstate=kwargs.get("sqlstate"),
483497
sfqid=kwargs.get("sfqid"),
484498
)
@@ -491,7 +505,8 @@ def __init__(self, **kwargs) -> None:
491505
Error.__init__(
492506
self,
493507
msg=kwargs.get("msg") or "HTTP 408: Request Timeout",
494-
errno=kwargs.get("errno"),
508+
errno=ER_HTTP_GENERAL_ERROR + kwargs.get("errno", 0),
509+
errtype=TelemetryField.HTTP_EXCEPTION,
495510
sqlstate=kwargs.get("sqlstate"),
496511
sfqid=kwargs.get("sfqid"),
497512
)
@@ -504,7 +519,8 @@ def __init__(self, **kwargs) -> None:
504519
Error.__init__(
505520
self,
506521
msg=kwargs.get("msg") or "HTTP 400: Bad Request",
507-
errno=kwargs.get("errno"),
522+
errno=ER_HTTP_GENERAL_ERROR + kwargs.get("errno", 0),
523+
errtype=TelemetryField.HTTP_EXCEPTION,
508524
sqlstate=kwargs.get("sqlstate"),
509525
sfqid=kwargs.get("sfqid"),
510526
)
@@ -517,7 +533,8 @@ def __init__(self, **kwargs) -> None:
517533
Error.__init__(
518534
self,
519535
msg=kwargs.get("msg") or "HTTP 502: Bad Gateway",
520-
errno=kwargs.get("errno"),
536+
errno=ER_HTTP_GENERAL_ERROR + kwargs.get("errno", 0),
537+
errtype=TelemetryField.HTTP_EXCEPTION,
521538
sqlstate=kwargs.get("sqlstate"),
522539
sfqid=kwargs.get("sfqid"),
523540
)
@@ -530,7 +547,8 @@ def __init__(self, **kwargs) -> None:
530547
Error.__init__(
531548
self,
532549
msg=kwargs.get("msg") or "HTTP 405: Method not allowed",
533-
errno=kwargs.get("errno"),
550+
errno=ER_HTTP_GENERAL_ERROR + kwargs.get("errno", 0),
551+
errtype=TelemetryField.HTTP_EXCEPTION,
534552
sqlstate=kwargs.get("sqlstate"),
535553
sfqid=kwargs.get("sfqid"),
536554
)
@@ -543,7 +561,8 @@ def __init__(self, **kwargs) -> None:
543561
Error.__init__(
544562
self,
545563
msg=kwargs.get("msg") or "HTTP 429: Too Many Requests",
546-
errno=kwargs.get("errno"),
564+
errno=ER_HTTP_GENERAL_ERROR + kwargs.get("errno", 0),
565+
errtype=TelemetryField.HTTP_EXCEPTION,
547566
sqlstate=kwargs.get("sqlstate"),
548567
sfqid=kwargs.get("sfqid"),
549568
)
@@ -568,7 +587,8 @@ def __init__(self, **kwargs) -> None:
568587
Error.__init__(
569588
self,
570589
msg=kwargs.get("msg") or f"HTTP {code}",
571-
errno=kwargs.get("errno"),
590+
errno=ER_HTTP_GENERAL_ERROR + kwargs.get("errno", 0),
591+
errtype=TelemetryField.HTTP_EXCEPTION,
572592
sqlstate=kwargs.get("sqlstate"),
573593
sfqid=kwargs.get("sfqid"),
574594
)

src/snowflake/connector/network.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
IncompleteRead,
4141
urlencode,
4242
urlparse,
43+
urlsplit,
4344
)
4445
from .constants import (
4546
_CONNECTIVITY_ERR_MSG,
@@ -65,6 +66,7 @@
6566
ER_FAILED_TO_CONNECT_TO_DB,
6667
ER_FAILED_TO_RENEW_SESSION,
6768
ER_FAILED_TO_REQUEST,
69+
ER_HTTP_GENERAL_ERROR,
6870
ER_RETRYABLE_CODE,
6971
)
7072
from .errors import (
@@ -74,7 +76,7 @@
7476
Error,
7577
ForbiddenError,
7678
GatewayTimeoutError,
77-
InterfaceError,
79+
HttpError,
7880
InternalServerError,
7981
MethodNotAllowed,
8082
OperationalError,
@@ -236,10 +238,10 @@ def raise_failed_request_error(
236238
Error.errorhandler_wrapper(
237239
connection,
238240
None,
239-
InterfaceError,
241+
HttpError,
240242
{
241-
"msg": f"{response.status_code} {response.reason}: {method} {url}",
242-
"errno": ER_FAILED_TO_REQUEST,
243+
"msg": f"{response.status_code} {response.reason}: {method} {urlsplit(url).netloc}{urlsplit(url).path}",
244+
"errno": ER_HTTP_GENERAL_ERROR + response.status_code,
243245
"sqlstate": SQLSTATE_CONNECTION_WAS_NOT_ESTABLISHED,
244246
},
245247
)
@@ -1033,6 +1035,14 @@ def _request_exec_wrapper(
10331035
retry_ctx.increment()
10341036

10351037
reason = getattr(cause, "errno", 0)
1038+
if reason is None:
1039+
reason = 0
1040+
else:
1041+
reason = (
1042+
reason - ER_HTTP_GENERAL_ERROR
1043+
if reason >= ER_HTTP_GENERAL_ERROR
1044+
else reason
1045+
)
10361046
retry_ctx.retry_reason = reason
10371047

10381048
if "Connection aborted" in repr(e) and "ECONNRESET" in repr(e):

src/snowflake/connector/telemetry.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class TelemetryField(Enum):
2626
TIME_PARSING_CHUNKS = "client_time_parsing_chunks"
2727
SQL_EXCEPTION = "client_sql_exception"
2828
OCSP_EXCEPTION = "client_ocsp_exception"
29+
HTTP_EXCEPTION = "client_http_exception"
2930
GET_PARTITIONS_USED = "client_get_partitions_used"
3031
EMPTY_SEQ_INTERPOLATION = "client_pyformat_empty_seq_interpolation"
3132
# fetch_pandas_* usage

test/integ/test_connection.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
ER_NO_ACCOUNT_NAME,
3131
ER_NOT_IMPLICITY_SNOWFLAKE_DATATYPE,
3232
)
33-
from snowflake.connector.errors import Error, InterfaceError
33+
from snowflake.connector.errors import Error
3434
from snowflake.connector.network import APPLICATION_SNOWSQL, ReauthenticationRequest
3535
from snowflake.connector.sqlstate import SQLSTATE_FEATURE_NOT_SUPPORTED
3636
from snowflake.connector.telemetry import TelemetryField
@@ -54,6 +54,11 @@
5454
except ImportError: # Keep olddrivertest from breaking
5555
ER_FAILED_PROCESSING_QMARK = 252012
5656

57+
try:
58+
from snowflake.connector.errors import HttpError
59+
except ImportError:
60+
pass
61+
5762

5863
def test_basic(conn_testaccount):
5964
"""Basic Connection test."""
@@ -530,7 +535,7 @@ def exe(sql):
530535
@pytest.mark.timeout(15)
531536
@pytest.mark.skipolddriver
532537
def test_invalid_account_timeout():
533-
with pytest.raises(InterfaceError):
538+
with pytest.raises(HttpError):
534539
snowflake.connector.connect(
535540
account="bogus", user="test", password="test", login_timeout=5
536541
)
@@ -569,7 +574,7 @@ def test_eu_connection(tmpdir):
569574
import os
570575

571576
os.environ["SF_OCSP_RESPONSE_CACHE_SERVER_ENABLED"] = "true"
572-
with pytest.raises(InterfaceError):
577+
with pytest.raises(HttpError):
573578
# must reach Snowflake
574579
snowflake.connector.connect(
575580
account="testaccount1234",
@@ -593,7 +598,7 @@ def test_us_west_connection(tmpdir):
593598
Notes:
594599
Region is deprecated.
595600
"""
596-
with pytest.raises(InterfaceError):
601+
with pytest.raises(HttpError):
597602
# must reach Snowflake
598603
snowflake.connector.connect(
599604
account="testaccount1234",

test/unit/test_connection.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from snowflake.connector.connection import DEFAULT_CONFIGURATION
2222
from snowflake.connector.errors import (
2323
Error,
24-
InterfaceError,
24+
HttpError,
2525
OperationalError,
2626
ProgrammingError,
2727
)
@@ -365,7 +365,7 @@ def test_invalid_backoff_policy():
365365
# passing a non-generator function should not work
366366
_ = fake_connector(backoff_policy=lambda: None)
367367

368-
with pytest.raises(InterfaceError):
368+
with pytest.raises(HttpError):
369369
# passing a generator function should make it pass config and error during connection
370370
_ = fake_connector(backoff_policy=zero_backoff)
371371

test/unit/test_network.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77

88
import pytest
99

10+
from snowflake.connector.errors import HttpError
1011
from src.snowflake.connector.network import SnowflakeRestfulJsonEncoder
1112

1213
try:
13-
from snowflake.connector import Error, InterfaceError
14+
from snowflake.connector import Error
1415
from snowflake.connector.network import SnowflakeRestful
1516
from snowflake.connector.vendored.requests import HTTPError, Response
1617
except ImportError:
@@ -64,9 +65,9 @@ def test_fetch():
6465
== {}
6566
)
6667
assert rest.fetch(**default_parameters, no_retry=True) == {}
67-
# if no retry is set to False, the function raises an InterfaceError
68-
with pytest.raises(InterfaceError) as exc:
69-
assert rest.fetch(**default_parameters, no_retry=False)
68+
# if no retry is set to False, the function raises an HttpError
69+
with pytest.raises(HttpError):
70+
rest.fetch(**default_parameters, no_retry=False)
7071

7172

7273
@pytest.mark.parametrize(

test/unit/test_result_batch.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import pytest
1010

11-
from snowflake.connector import DatabaseError, InterfaceError
11+
from snowflake.connector import DatabaseError
1212
from snowflake.connector.compat import (
1313
BAD_GATEWAY,
1414
BAD_REQUEST,
@@ -23,13 +23,14 @@
2323
)
2424
from snowflake.connector.errorcode import (
2525
ER_FAILED_TO_CONNECT_TO_DB,
26-
ER_FAILED_TO_REQUEST,
26+
ER_HTTP_GENERAL_ERROR,
2727
)
2828
from snowflake.connector.errors import (
2929
BadGatewayError,
3030
BadRequest,
3131
ForbiddenError,
3232
GatewayTimeoutError,
33+
HttpError,
3334
InternalServerError,
3435
MethodNotAllowed,
3536
OtherHTTPRetryableError,
@@ -127,10 +128,10 @@ def test_non_200_response_download(status_code):
127128
mock_get.return_value = create_mock_response(status_code)
128129

129130
with mock.patch("time.sleep", return_value=None):
130-
with pytest.raises(InterfaceError) as ex:
131+
with pytest.raises(HttpError) as ex:
131132
_ = result_batch._download()
132133
error = ex.value
133-
assert error.errno == ER_FAILED_TO_REQUEST
134+
assert error.errno == ER_HTTP_GENERAL_ERROR + status_code
134135
assert error.sqlstate == SQLSTATE_CONNECTION_WAS_NOT_ESTABLISHED
135136
assert mock_get.call_count == MAX_DOWNLOAD_RETRY
136137

test/unit/test_retry_network.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
DatabaseError,
3030
Error,
3131
ForbiddenError,
32-
InterfaceError,
32+
HttpError,
3333
OperationalError,
3434
OtherHTTPRetryableError,
3535
ServiceUnavailableError,
@@ -217,7 +217,7 @@ def test_request_exec():
217217

218218
# unauthorized
219219
type(request_mock).status_code = PropertyMock(return_value=UNAUTHORIZED)
220-
with pytest.raises(InterfaceError):
220+
with pytest.raises(HttpError):
221221
rest._request_exec(session=session, **default_parameters)
222222

223223
# unauthorized with catch okta unauthorized error

0 commit comments

Comments
 (0)