Skip to content

Commit 5f7af94

Browse files
committed
fix(transport): Add asyncio integration check to async transport
GH-4601
1 parent 6dd8138 commit 5f7af94

File tree

3 files changed

+135
-114
lines changed

3 files changed

+135
-114
lines changed

sentry_sdk/transport.py

Lines changed: 118 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -585,9 +585,115 @@ def flush(
585585
self._worker.flush(timeout, callback)
586586

587587

588+
class HttpTransport(BaseHttpTransport):
589+
if TYPE_CHECKING:
590+
_pool: Union[PoolManager, ProxyManager]
591+
592+
def _get_pool_options(self: Self) -> Dict[str, Any]:
593+
594+
num_pools = self.options.get("_experiments", {}).get("transport_num_pools")
595+
options = {
596+
"num_pools": 2 if num_pools is None else int(num_pools),
597+
"cert_reqs": "CERT_REQUIRED",
598+
"timeout": urllib3.Timeout(total=self.TIMEOUT),
599+
}
600+
601+
socket_options: Optional[List[Tuple[int, int, int | bytes]]] = None
602+
603+
if self.options["socket_options"] is not None:
604+
socket_options = self.options["socket_options"]
605+
606+
if self.options["keep_alive"]:
607+
if socket_options is None:
608+
socket_options = []
609+
610+
used_options = {(o[0], o[1]) for o in socket_options}
611+
for default_option in KEEP_ALIVE_SOCKET_OPTIONS:
612+
if (default_option[0], default_option[1]) not in used_options:
613+
socket_options.append(default_option)
614+
615+
if socket_options is not None:
616+
options["socket_options"] = socket_options
617+
618+
options["ca_certs"] = (
619+
self.options["ca_certs"] # User-provided bundle from the SDK init
620+
or os.environ.get("SSL_CERT_FILE")
621+
or os.environ.get("REQUESTS_CA_BUNDLE")
622+
or certifi.where()
623+
)
624+
625+
options["cert_file"] = self.options["cert_file"] or os.environ.get(
626+
"CLIENT_CERT_FILE"
627+
)
628+
options["key_file"] = self.options["key_file"] or os.environ.get(
629+
"CLIENT_KEY_FILE"
630+
)
631+
632+
return options
633+
634+
def _make_pool(self: Self) -> Union[PoolManager, ProxyManager]:
635+
if self.parsed_dsn is None:
636+
raise ValueError("Cannot create HTTP-based transport without valid DSN")
637+
638+
proxy = None
639+
no_proxy = self._in_no_proxy(self.parsed_dsn)
640+
641+
# try HTTPS first
642+
https_proxy = self.options["https_proxy"]
643+
if self.parsed_dsn.scheme == "https" and (https_proxy != ""):
644+
proxy = https_proxy or (not no_proxy and getproxies().get("https"))
645+
646+
# maybe fallback to HTTP proxy
647+
http_proxy = self.options["http_proxy"]
648+
if not proxy and (http_proxy != ""):
649+
proxy = http_proxy or (not no_proxy and getproxies().get("http"))
650+
651+
opts = self._get_pool_options()
652+
653+
if proxy:
654+
proxy_headers = self.options["proxy_headers"]
655+
if proxy_headers:
656+
opts["proxy_headers"] = proxy_headers
657+
658+
if proxy.startswith("socks"):
659+
use_socks_proxy = True
660+
try:
661+
# Check if PySocks dependency is available
662+
from urllib3.contrib.socks import SOCKSProxyManager
663+
except ImportError:
664+
use_socks_proxy = False
665+
logger.warning(
666+
"You have configured a SOCKS proxy (%s) but support for SOCKS proxies is not installed. Disabling proxy support. Please add `PySocks` (or `urllib3` with the `[socks]` extra) to your dependencies.",
667+
proxy,
668+
)
669+
670+
if use_socks_proxy:
671+
return SOCKSProxyManager(proxy, **opts)
672+
else:
673+
return urllib3.PoolManager(**opts)
674+
else:
675+
return urllib3.ProxyManager(proxy, **opts)
676+
else:
677+
return urllib3.PoolManager(**opts)
678+
679+
def _request(
680+
self: Self,
681+
method: str,
682+
endpoint_type: EndpointType,
683+
body: Any,
684+
headers: Mapping[str, str],
685+
) -> urllib3.BaseHTTPResponse:
686+
return self._pool.request(
687+
method,
688+
self._auth.get_api_url(endpoint_type),
689+
body=body,
690+
headers=headers,
691+
)
692+
693+
588694
if not ASYNC_TRANSPORT_ENABLED:
589695
# Sorry, no AsyncHttpTransport for you
590-
AsyncHttpTransport = BaseHttpTransport
696+
AsyncHttpTransport = HttpTransport
591697

592698
else:
593699

@@ -801,112 +907,6 @@ def kill(self: Self) -> Optional[asyncio.Task[None]]: # type: ignore
801907
return None
802908

803909

804-
class HttpTransport(BaseHttpTransport):
805-
if TYPE_CHECKING:
806-
_pool: Union[PoolManager, ProxyManager]
807-
808-
def _get_pool_options(self: Self) -> Dict[str, Any]:
809-
810-
num_pools = self.options.get("_experiments", {}).get("transport_num_pools")
811-
options = {
812-
"num_pools": 2 if num_pools is None else int(num_pools),
813-
"cert_reqs": "CERT_REQUIRED",
814-
"timeout": urllib3.Timeout(total=self.TIMEOUT),
815-
}
816-
817-
socket_options: Optional[List[Tuple[int, int, int | bytes]]] = None
818-
819-
if self.options["socket_options"] is not None:
820-
socket_options = self.options["socket_options"]
821-
822-
if self.options["keep_alive"]:
823-
if socket_options is None:
824-
socket_options = []
825-
826-
used_options = {(o[0], o[1]) for o in socket_options}
827-
for default_option in KEEP_ALIVE_SOCKET_OPTIONS:
828-
if (default_option[0], default_option[1]) not in used_options:
829-
socket_options.append(default_option)
830-
831-
if socket_options is not None:
832-
options["socket_options"] = socket_options
833-
834-
options["ca_certs"] = (
835-
self.options["ca_certs"] # User-provided bundle from the SDK init
836-
or os.environ.get("SSL_CERT_FILE")
837-
or os.environ.get("REQUESTS_CA_BUNDLE")
838-
or certifi.where()
839-
)
840-
841-
options["cert_file"] = self.options["cert_file"] or os.environ.get(
842-
"CLIENT_CERT_FILE"
843-
)
844-
options["key_file"] = self.options["key_file"] or os.environ.get(
845-
"CLIENT_KEY_FILE"
846-
)
847-
848-
return options
849-
850-
def _make_pool(self: Self) -> Union[PoolManager, ProxyManager]:
851-
if self.parsed_dsn is None:
852-
raise ValueError("Cannot create HTTP-based transport without valid DSN")
853-
854-
proxy = None
855-
no_proxy = self._in_no_proxy(self.parsed_dsn)
856-
857-
# try HTTPS first
858-
https_proxy = self.options["https_proxy"]
859-
if self.parsed_dsn.scheme == "https" and (https_proxy != ""):
860-
proxy = https_proxy or (not no_proxy and getproxies().get("https"))
861-
862-
# maybe fallback to HTTP proxy
863-
http_proxy = self.options["http_proxy"]
864-
if not proxy and (http_proxy != ""):
865-
proxy = http_proxy or (not no_proxy and getproxies().get("http"))
866-
867-
opts = self._get_pool_options()
868-
869-
if proxy:
870-
proxy_headers = self.options["proxy_headers"]
871-
if proxy_headers:
872-
opts["proxy_headers"] = proxy_headers
873-
874-
if proxy.startswith("socks"):
875-
use_socks_proxy = True
876-
try:
877-
# Check if PySocks dependency is available
878-
from urllib3.contrib.socks import SOCKSProxyManager
879-
except ImportError:
880-
use_socks_proxy = False
881-
logger.warning(
882-
"You have configured a SOCKS proxy (%s) but support for SOCKS proxies is not installed. Disabling proxy support. Please add `PySocks` (or `urllib3` with the `[socks]` extra) to your dependencies.",
883-
proxy,
884-
)
885-
886-
if use_socks_proxy:
887-
return SOCKSProxyManager(proxy, **opts)
888-
else:
889-
return urllib3.PoolManager(**opts)
890-
else:
891-
return urllib3.ProxyManager(proxy, **opts)
892-
else:
893-
return urllib3.PoolManager(**opts)
894-
895-
def _request(
896-
self: Self,
897-
method: str,
898-
endpoint_type: EndpointType,
899-
body: Any,
900-
headers: Mapping[str, str],
901-
) -> urllib3.BaseHTTPResponse:
902-
return self._pool.request(
903-
method,
904-
self._auth.get_api_url(endpoint_type),
905-
body=body,
906-
headers=headers,
907-
)
908-
909-
910910
if not HTTP2_ENABLED:
911911
# Sorry, no Http2Transport for you
912912
class Http2Transport(HttpTransport):
@@ -1047,14 +1047,24 @@ def make_transport(options: Dict[str, Any]) -> Optional[Transport]:
10471047

10481048
use_http2_transport = options.get("_experiments", {}).get("transport_http2", False)
10491049
use_async_transport = options.get("_experiments", {}).get("transport_async", False)
1050+
async_integration = any(
1051+
integration.__class__.__name__ == "AsyncioIntegration"
1052+
for integration in options.get("integrations", [])
1053+
)
1054+
10501055
# By default, we use the http transport class
10511056
transport_cls: Type[Transport] = (
10521057
Http2Transport if use_http2_transport else HttpTransport
10531058
)
10541059
if use_async_transport and ASYNC_TRANSPORT_ENABLED:
10551060
try:
10561061
asyncio.get_running_loop()
1057-
transport_cls = AsyncHttpTransport
1062+
if async_integration:
1063+
transport_cls = AsyncHttpTransport
1064+
else:
1065+
logger.warning(
1066+
"You tried to use AsyncHttpTransport but the AsyncioIntegration is not enabled. Falling back to sync transport."
1067+
)
10581068
except RuntimeError:
10591069
# No event loop running, fall back to sync transport
10601070
logger.warning("No event loop running, falling back to sync transport.")

tests/test_client.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
from sentry_sdk.spotlight import DEFAULT_SPOTLIGHT_URL
2424
from sentry_sdk.utils import capture_internal_exception
2525
from sentry_sdk.integrations.executing import ExecutingIntegration
26+
from sentry_sdk.integrations.asyncio import AsyncioIntegration
27+
2628
from sentry_sdk.transport import Transport, AsyncHttpTransport
2729
from sentry_sdk.serializer import MAX_DATABAG_BREADTH
2830
from sentry_sdk.consts import DEFAULT_MAX_BREADCRUMBS, DEFAULT_MAX_VALUE_LENGTH
@@ -1693,7 +1695,10 @@ async def test_async_proxy(monkeypatch, testcase):
16931695
if testcase.get("env_no_proxy") is not None:
16941696
monkeypatch.setenv("NO_PROXY", testcase["env_no_proxy"])
16951697

1696-
kwargs = {"_experiments": {"transport_async": True}}
1698+
kwargs = {
1699+
"_experiments": {"transport_async": True},
1700+
"integrations": [AsyncioIntegration()],
1701+
}
16971702

16981703
if testcase["arg_http_proxy"] is not None:
16991704
kwargs["http_proxy"] = testcase["arg_http_proxy"]
@@ -1795,7 +1800,10 @@ async def test_async_socks_proxy(testcase):
17951800
# These are just the same tests as the sync ones, but they need to be run in an event loop
17961801
# and respect the shutdown behavior of the async transport
17971802

1798-
kwargs = {"_experiments": {"transport_async": True}}
1803+
kwargs = {
1804+
"_experiments": {"transport_async": True},
1805+
"integrations": [AsyncioIntegration()],
1806+
}
17991807

18001808
if testcase["arg_http_proxy"] is not None:
18011809
kwargs["http_proxy"] = testcase["arg_http_proxy"]

tests/test_transport.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
AsyncHttpTransport,
3434
)
3535
from sentry_sdk.integrations.logging import LoggingIntegration, ignore_logger
36+
from sentry_sdk.integrations.asyncio import AsyncioIntegration
3637

3738

3839
server = None
@@ -183,6 +184,7 @@ async def test_transport_works_async(
183184
client = make_client(
184185
debug=debug,
185186
_experiments=experiments,
187+
integrations=[AsyncioIntegration()],
186188
)
187189

188190
if use_pickle:
@@ -812,7 +814,7 @@ async def test_async_transport_background_thread_capture(
812814
"""Test capture_envelope from background threads uses run_coroutine_threadsafe"""
813815
caplog.set_level(logging.DEBUG)
814816
experiments = {"transport_async": True}
815-
client = make_client(_experiments=experiments)
817+
client = make_client(_experiments=experiments, integrations=[AsyncioIntegration()])
816818
assert isinstance(client.transport, AsyncHttpTransport)
817819
sentry_sdk.get_global_scope().set_client(client)
818820
captured_from_thread = []
@@ -843,7 +845,7 @@ async def test_async_transport_event_loop_closed_scenario(
843845
"""Test behavior when trying to capture after event loop context ends"""
844846
caplog.set_level(logging.DEBUG)
845847
experiments = {"transport_async": True}
846-
client = make_client(_experiments=experiments)
848+
client = make_client(_experiments=experiments, integrations=[AsyncioIntegration()])
847849
sentry_sdk.get_global_scope().set_client(client)
848850
original_loop = client.transport.loop
849851

@@ -869,7 +871,7 @@ async def test_async_transport_concurrent_requests(
869871
"""Test multiple simultaneous envelope submissions"""
870872
caplog.set_level(logging.DEBUG)
871873
experiments = {"transport_async": True}
872-
client = make_client(_experiments=experiments)
874+
client = make_client(_experiments=experiments, integrations=[AsyncioIntegration()])
873875
assert isinstance(client.transport, AsyncHttpTransport)
874876
sentry_sdk.get_global_scope().set_client(client)
875877

@@ -891,7 +893,7 @@ async def test_async_transport_rate_limiting_with_concurrency(
891893
):
892894
"""Test async transport rate limiting with concurrent requests"""
893895
experiments = {"transport_async": True}
894-
client = make_client(_experiments=experiments)
896+
client = make_client(_experiments=experiments, integrations=[AsyncioIntegration()])
895897

896898
assert isinstance(client.transport, AsyncHttpTransport)
897899
sentry_sdk.get_global_scope().set_client(client)
@@ -929,6 +931,7 @@ async def test_async_two_way_ssl_authentication():
929931
cert_file=cert_file,
930932
key_file=key_file,
931933
_experiments={"transport_async": True},
934+
integrations=[AsyncioIntegration()],
932935
)
933936
assert isinstance(client.transport, AsyncHttpTransport)
934937

0 commit comments

Comments
 (0)