From 3edc769eab2e6dac16c2169a3591b7b0afd8eb03 Mon Sep 17 00:00:00 2001 From: AP Date: Mon, 5 Oct 2026 11:16:40 +0530 Subject: [PATCH] fix(transport): raise TransportResponseError on unexpected HTTP status HttpTransportCore._handle_response logged unexpected non-2xx statuses (e.g. 403/404 from a network middlebox) and recorded the loss, but never raised, so custom/wrapper transports calling the synchronous send path had no way to detect the failed delivery and trigger failover routing. Raise TransportResponseError after recording the loss, so a failed delivery surfaces as an exception like network errors already do. 429 keeps its rate-limit handling and 413 its deliberate-drop semantics; neither raises. Fixes #7844. --- sentry_sdk/transport.py | 16 ++++++++++++++++ tests/test_transport.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/sentry_sdk/transport.py b/sentry_sdk/transport.py index 3f89e8c418..abc8bd48a7 100644 --- a/sentry_sdk/transport.py +++ b/sentry_sdk/transport.py @@ -211,6 +211,15 @@ def _parse_rate_limits( continue +class TransportResponseError(Exception): + """Raised when the HTTP transport receives an unexpected response status. + + A non-2xx status (other than 429 rate limiting) means the envelope was not + accepted. Raising lets custom/wrapper transports detect the failed + delivery and fail over to another route instead of assuming success. + """ + + class HttpTransportCore(Transport): """Shared base class for sync and async transports.""" @@ -404,6 +413,13 @@ def _handle_response( self._handle_request_error( envelope=envelope, loss_reason="status_{}".format(response.status) ) + # Surface the failed delivery so custom/wrapper transports can + # catch it and fail over (e.g. a 403/404 from a network middlebox). + # 429 is handled via rate limits above and 413 is a deliberate + # drop, so neither raises here. + raise TransportResponseError( + "Unexpected status code: {}".format(response.status) + ) def _update_headers( self: "Self", diff --git a/tests/test_transport.py b/tests/test_transport.py index 3db95c06f6..f21a629727 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -1110,3 +1110,40 @@ async def send_message(i): # New request should be dropped due to rate limiting assert len(capturing_server.captured) == 0 await client.close_async(timeout=2.0) + + +from sentry_sdk.consts import EndpointType +from sentry_sdk.transport import TransportResponseError + + +@pytest.mark.parametrize("status_code", [403, 404, 500]) +def test_unexpected_status_code_raises(capturing_server, make_client, status_code): + # https://github.com/getsentry/sentry-python/issues/7844 + # A non-2xx response (e.g. 403/404 from a network middlebox) is a failed + # delivery and must raise, so wrapper transports can catch it and fail + # over instead of assuming the event was sent. + client = make_client() + capturing_server.respond_with(code=status_code) + + with pytest.raises(TransportResponseError, match="Unexpected status code"): + client.transport._send_request( + body=b"{}", + headers={}, + endpoint_type=EndpointType.ENVELOPE, + ) + + +@pytest.mark.parametrize("status_code", [413, 429]) +def test_expected_error_status_codes_do_not_raise( + capturing_server, make_client, status_code +): + # 413 (envelope too large) is a deliberate drop and 429 is handled via + # rate limits; neither should raise. + client = make_client() + capturing_server.respond_with(code=status_code) + + client.transport._send_request( + body=b"{}", + headers={}, + endpoint_type=EndpointType.ENVELOPE, + )