From abc57bcafa266dab93abfeb388431b9a006e8b42 Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Wed, 5 Aug 2026 16:49:19 +0200 Subject: [PATCH 01/37] fix(boto3): Inject trace headers before SigV4 signing - merge Sentry baggage with existing vendor (e.g. Datadog) baggage in botocore's`before-sign` hook; avoiding post-sign header tampering that invalidates the SigV4 signature. - Skip propagation for presigned requests Fixes: #7031 & PY-2667 --- sentry_sdk/integrations/boto3.py | 57 ++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/boto3.py b/sentry_sdk/integrations/boto3.py index 69deefc7b7..844ec5f518 100644 --- a/sentry_sdk/integrations/boto3.py +++ b/sentry_sdk/integrations/boto3.py @@ -6,8 +6,12 @@ from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version from sentry_sdk.scope import should_send_default_pii from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.tracing import BAGGAGE_HEADER_NAME, Span +from sentry_sdk.tracing_utils import ( + add_sentry_baggage_to_headers, + has_span_streaming_enabled, + should_propagate_trace, +) from sentry_sdk.utils import ( capture_internal_exceptions, parse_url, @@ -49,6 +53,8 @@ def sentry_patched_init( "request-created", partial(_sentry_request_created, service_id=service_id), ) + # run after other `before-sign` handlers, allowing it to see and preserve existing baggage. + meta.events.register_last("before-sign", _sentry_before_sign) meta.events.register("after-call", _sentry_after_call) meta.events.register("after-call-error", _sentry_after_call_error) @@ -114,6 +120,53 @@ def _sentry_request_created( request.context["_sentrysdk_span"] = span +def _sentry_before_sign( + request: "AWSRequest", signature_version: "Any", **kwargs: "Any" +) -> None: + client = sentry_sdk.get_client() + if client.get_integration(Boto3Integration) is None: + return + + with capture_internal_exceptions(): + # presigned requests are executed later by another caller. Adding propagation + # headers here would make those headers part of the signature, requiring the caller to reproduce the same values. + if isinstance(signature_version, str) and signature_version.endswith( + ("-query", "-presign-post") + ): + return + + if request.url is None or not should_propagate_trace(client, request.url): + return + + def _replace_header(request: "AWSRequest", key: str, value: str) -> None: + if key in request.headers: + del request.headers[key] + request.headers[key] = value + + # use span associated with this botocore request + span = request.context.get("_sentrysdk_span") + + headers = sentry_sdk.get_current_scope().iter_trace_propagation_headers( + span=span + ) + for header_name, header_value in headers: + if header_name != BAGGAGE_HEADER_NAME: + # normal headers (e.g. `sentry-trace`) are non-shared, so replace stale values + _replace_header(request, header_name, header_value) + continue + + # merge existing `baggage` values under single header + existing_values = request.headers.get_all(BAGGAGE_HEADER_NAME, []) + combined_baggage = { + BAGGAGE_HEADER_NAME: ",".join(str(value) for value in existing_values) + } + # preserve third-party baggage, replace stale `sentry-*` values + add_sentry_baggage_to_headers(combined_baggage, header_value) + _replace_header( + request, BAGGAGE_HEADER_NAME, combined_baggage[BAGGAGE_HEADER_NAME] + ) + + def _sentry_after_call( context: "Dict[str, Any]", parsed: "Dict[str, Any]", **kwargs: "Any" ) -> None: From 78f1ee6de251252ef7a266c4e6c99c9818d0f780 Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Wed, 5 Aug 2026 17:46:31 +0200 Subject: [PATCH 02/37] fix(stdlib): Preserve signed headers during trace propagation - trace-header injection is delayed until `endheaders()` after all request headers are available. - avoid duplicate propagation headers. - no tampering/modifying of already signed baggage Fixes: #7031 & PY-2667 --- sentry_sdk/integrations/stdlib.py | 98 ++++++++++++++++++++++++++----- 1 file changed, 83 insertions(+), 15 deletions(-) diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index 4de3819a77..e7c1080626 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -10,7 +10,7 @@ from sentry_sdk.integrations import Integration from sentry_sdk.scope import add_global_event_processor, should_send_default_pii from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span +from sentry_sdk.tracing import BAGGAGE_HEADER_NAME, Span from sentry_sdk.tracing_utils import ( EnvironHeaders, add_http_request_source, @@ -28,7 +28,7 @@ ) if TYPE_CHECKING: - from typing import Any, Callable, Dict, List, Optional, Union + from typing import Any, Callable, Dict, List, Optional, Set, Union from sentry_sdk._types import Event, Hint @@ -61,6 +61,41 @@ def add_python_runtime_context( return event +def _aws_sigv4_signed_headers(buffer: "Optional[List[bytes]]") -> "Set[str]": + if buffer is None: + return set() + for line in buffer: + name, separator, value = line.partition(b":") + if not separator or name.lower() != b"authorization": + continue + + value = value.lstrip() + if not value.startswith((b"AWS4-HMAC-SHA256", b"AWS4-ECDSA-P256-SHA256")): + return set() + + for part in value.split(b","): + part = part.strip() + if part.startswith(b"SignedHeaders="): + _, _, header_names = part.partition(b"=") + return { + header.decode("ascii", "ignore").lower() + for header in header_names.split(b";") + if header + } + return set() + + +def _request_header_names(buffer: "Optional[List[bytes]]") -> "Set[str]": + if buffer is None: + return set() + names = set() + for line in buffer: + name, separator, _ = line.partition(b":") + if separator: + names.add(name.decode("ascii", "ignore").lower()) + return names + + def _complete_span(span: "Union[Span, StreamedSpan]") -> None: if isinstance(span, StreamedSpan): with capture_internal_exceptions(): @@ -74,6 +109,7 @@ def _complete_span(span: "Union[Span, StreamedSpan]") -> None: def _install_httplib() -> None: real_putrequest = HTTPConnection.putrequest + real_endheaders = HTTPConnection.endheaders real_getresponse = HTTPConnection.getresponse real_read = HTTPResponse.read real_close = HTTPResponse.close @@ -157,26 +193,57 @@ def putrequest( set_on_span(SPANDATA.NETWORK_PEER_ADDRESS, self.host) set_on_span(SPANDATA.NETWORK_PEER_PORT, self.port) - rv = real_putrequest(self, method, url, *args, **kwargs) + try: + rv = real_putrequest(self, method, url, *args, **kwargs) + except BaseException: + self._sentrysdk_trace_headers = () # type: ignore[attr-defined] + self._sentrysdk_trace_url = None # type: ignore[attr-defined] + raise if should_propagate_trace(client, real_url): - for ( - key, - value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( - span=span - ): - logger.debug( - "[Tracing] Adding `{key}` header {value} to outgoing request to {real_url}.".format( - key=key, value=value, real_url=real_url - ) - ) - self.putheader(key, value) + self._sentrysdk_trace_headers = tuple( # type: ignore[attr-defined] + sentry_sdk.get_current_scope().iter_trace_propagation_headers(span=span) + ) + self._sentrysdk_trace_url = real_url # type: ignore[attr-defined] + else: + self._sentrysdk_trace_headers = () # type: ignore[attr-defined] + self._sentrysdk_trace_url = None # type: ignore[attr-defined] self._sentrysdk_span = span # type: ignore[attr-defined] return rv + def endheaders(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": + trace_headers = getattr(self, "_sentrysdk_trace_headers", ()) + real_url = getattr(self, "_sentrysdk_trace_url", None) + + try: + if trace_headers: + request_buffer = getattr(self, "_buffer", None) + existing_headers = _request_header_names(request_buffer) + signed_headers = _aws_sigv4_signed_headers(request_buffer) + + for header_name, header_value in trace_headers: + normalized_header = header_name.lower() + # do not mutate headers already signed by SigV4. + if normalized_header in existing_headers and ( + normalized_header != BAGGAGE_HEADER_NAME + or normalized_header in signed_headers + ): + continue + + logger.debug( + "[Tracing] Adding `{key}` header {value} to outgoing request to {real_url}.".format( + key=header_name, value=header_value, real_url=real_url + ) + ) + self.putheader(header_name, header_value) + + return real_endheaders(self, *args, **kwargs) + finally: + self._sentrysdk_trace_headers = () # type: ignore[attr-defined] + self._sentrysdk_trace_url = None # type: ignore[attr-defined] + def getresponse(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": span = getattr(self, "_sentrysdk_span", None) @@ -233,6 +300,7 @@ def close(self: "HTTPResponse") -> None: _complete_span(span) HTTPConnection.putrequest = putrequest # type: ignore[method-assign] + HTTPConnection.endheaders = endheaders # type: ignore[method-assign] HTTPConnection.getresponse = getresponse # type: ignore[method-assign] HTTPResponse.read = read # type: ignore[method-assign] HTTPResponse.close = close # type: ignore[assignment,method-assign] From 88fdd7d7e9ad3b9b0a0fe9b9b0048c277010e82a Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Thu, 6 Aug 2026 11:28:21 +0200 Subject: [PATCH 03/37] test(boto3): add tests covering trace propagation + SigV4 signing Refs: #7031 & PY-2667 --- .../boto3/test_trace_propagation.py | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 tests/integrations/boto3/test_trace_propagation.py diff --git a/tests/integrations/boto3/test_trace_propagation.py b/tests/integrations/boto3/test_trace_propagation.py new file mode 100644 index 0000000000..98e1d1ccdc --- /dev/null +++ b/tests/integrations/boto3/test_trace_propagation.py @@ -0,0 +1,192 @@ +from http.server import BaseHTTPRequestHandler, HTTPServer +from threading import Thread +from urllib.parse import parse_qs, urlparse + +import boto3 +import pytest +from botocore.config import Config + +import sentry_sdk +from sentry_sdk.integrations.boto3 import Boto3Integration +from sentry_sdk.integrations.stdlib import StdlibIntegration + + +class _AwsRequestHandler(BaseHTTPRequestHandler): + requests = [] + + def do_HEAD(self): + self.__class__.requests.append(self.headers) + self.send_response(200) + self.end_headers() + + def log_message(self, format, *args): + pass + + +def _start_server(): + _AwsRequestHandler.requests = [] + server = HTTPServer(("127.0.0.1", 0), _AwsRequestHandler) + thread = Thread(target=server.serve_forever, daemon=True) + thread.start() + return server, thread + + +def _head_object(client, span_streaming): + if span_streaming: + with sentry_sdk.traces.start_span(name="incoming"): # type: ignore[attr-defined] + return client.head_object( + Bucket="example-bucket", + Key="example-key", + ) + + with sentry_sdk.start_transaction(name="incoming", sampled=True): + return client.head_object( + Bucket="example-bucket", + Key="example-key", + ) + + +@pytest.mark.parametrize("span_streaming", [False, True]) +def test_botocore_merges_propagation_before_sigv4_signing(sentry_init, span_streaming): + sentry_init( + traces_sample_rate=1.0, + trace_lifecycle="stream" if span_streaming else "static", + default_integrations=False, + integrations=[Boto3Integration(), StdlibIntegration()], + ) + + server, thread = _start_server() + + try: + client = boto3.client( # type: ignore[attr-defined] + "s3", + # connect to mock AWS server. + endpoint_url=f"http://127.0.0.1:{server.server_port}", + aws_access_key_id="test-access-key", + aws_secret_access_key="test-secret-key", + config=Config(signature_version="v4"), + ) + + def _inject_third_party_baggage(request, **kwargs): + request.headers.add_header( + "baggage", + "dd-origin=synthetics,sentry-trace_id=stale,sentry-sample_rand=0.100000", + ) + request.headers.add_header("baggage", "vendor=value") + + signed_request_headers = {} + + def capture_headers_after_instrumentation(request, **kwargs): + for header_name in ("baggage", "sentry-trace"): + signed_request_headers[header_name] = request.headers.get_all( + header_name + ) + + # register `before-sign` handler that adds third-party baggage. + client.meta.events.register("before-sign", _inject_third_party_baggage) + client.meta.events.register_last( + "before-sign", capture_headers_after_instrumentation + ) + + response = _head_object(client, span_streaming) + + assert response["ResponseMetadata"]["HTTPStatusCode"] == 200 + headers = _AwsRequestHandler.requests[-1] + + baggage_headers = headers.get_all("baggage") + assert baggage_headers is not None + assert len(baggage_headers) == 1 + assert baggage_headers == signed_request_headers["baggage"] + + baggage = baggage_headers[0] + # preserves third-party baggage. + assert "dd-origin=synthetics" in baggage + assert "vendor=value" in baggage + # add own `sentry-*` baggage. + assert "sentry-trace_id=" in baggage + assert "sentry-trace_id=stale" not in baggage + # replace stale values instead of duplicating them. + assert baggage.count("sentry-trace_id=") == 1 + assert baggage.count("sentry-sample_rand=") == 1 + + # adds single `sentry-trace` header. + sentry_trace_headers = headers.get_all("sentry-trace") + assert sentry_trace_headers is not None + assert len(sentry_trace_headers) == 1 + assert sentry_trace_headers == signed_request_headers["sentry-trace"] + + authorization = headers["Authorization"] + signed_headers = authorization.split("SignedHeaders=", 1)[1].split(",", 1)[0] + # both `baggage` and `sentry-trace` are signed. + assert "baggage" in signed_headers.split(";") + assert "sentry-trace" in signed_headers.split(";") + finally: + server.shutdown() + thread.join() + + +@pytest.mark.parametrize("span_streaming", [False, True]) +def test_botocore_without_boto3_integration_preserves_signed_baggage( + sentry_init, span_streaming +): + sentry_init( + traces_sample_rate=1.0, + trace_lifecycle="stream" if span_streaming else "static", + default_integrations=False, + integrations=[StdlibIntegration()], + ) + + server, thread = _start_server() + try: + client = boto3.client( # type: ignore[attr-defined] + "s3", + endpoint_url=f"http://127.0.0.1:{server.server_port}", + aws_access_key_id="test-access-key", + aws_secret_access_key="test-secret-key", + config=Config(signature_version="v4"), + ) + + def _inject_signed_baggage(request, **kwargs): + request.headers.add_header("baggage", "vendor=value") + + # register `before-sign` handler that third-party signed baggage. + client.meta.events.register("before-sign", _inject_signed_baggage) + + response = _head_object(client, span_streaming) + + assert response["ResponseMetadata"]["HTTPStatusCode"] == 200 + headers = _AwsRequestHandler.requests[-1] + # preserves third-party signed baggage. + assert headers.get_all("baggage") == ["vendor=value"] + # `httplib` still adds single `sentry-trace` header. + assert len(headers.get_all("sentry-trace")) == 1 + finally: + server.shutdown() + thread.join() + + +def test_presigned_urls_do_not_require_sentry_headers(sentry_init): + sentry_init( + traces_sample_rate=1.0, + default_integrations=False, + integrations=[Boto3Integration(), StdlibIntegration()], + ) + client = boto3.client( # type: ignore[attr-defined] + "s3", + aws_access_key_id="test-access-key", + aws_secret_access_key="test-secret-key", + config=Config(signature_version="s3v4"), + ) + + url = client.generate_presigned_url( + "get_object", + Params={"Bucket": "example-bucket", "Key": "example-key"}, + ExpiresIn=60, + ) + query = parse_qs(urlparse(url).query) + + # only `host` header is signed. + assert query["X-Amz-SignedHeaders"] == ["host"] + # no `sentry-*` or baggage are added. + assert "sentry-trace" not in url + assert "baggage" not in url From a1e5f9c75d56da7257297254a0215a8faa8d279c Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Thu, 6 Aug 2026 11:47:40 +0200 Subject: [PATCH 04/37] test(boto3): inline span_streaming logic Refs: #7031 & PY-2667 --- .../boto3/test_trace_propagation.py | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/tests/integrations/boto3/test_trace_propagation.py b/tests/integrations/boto3/test_trace_propagation.py index 98e1d1ccdc..e31edfeb06 100644 --- a/tests/integrations/boto3/test_trace_propagation.py +++ b/tests/integrations/boto3/test_trace_propagation.py @@ -31,21 +31,6 @@ def _start_server(): return server, thread -def _head_object(client, span_streaming): - if span_streaming: - with sentry_sdk.traces.start_span(name="incoming"): # type: ignore[attr-defined] - return client.head_object( - Bucket="example-bucket", - Key="example-key", - ) - - with sentry_sdk.start_transaction(name="incoming", sampled=True): - return client.head_object( - Bucket="example-bucket", - Key="example-key", - ) - - @pytest.mark.parametrize("span_streaming", [False, True]) def test_botocore_merges_propagation_before_sigv4_signing(sentry_init, span_streaming): sentry_init( @@ -88,7 +73,20 @@ def capture_headers_after_instrumentation(request, **kwargs): "before-sign", capture_headers_after_instrumentation ) - response = _head_object(client, span_streaming) + if span_streaming: + with sentry_sdk.traces.start_span( # type: ignore[attr-defined] + name="incoming" + ): + response = client.head_object( + Bucket="example-bucket", + Key="example-key", + ) + else: + with sentry_sdk.start_transaction(name="incoming", sampled=True): + response = client.head_object( + Bucket="example-bucket", + Key="example-key", + ) assert response["ResponseMetadata"]["HTTPStatusCode"] == 200 headers = _AwsRequestHandler.requests[-1] @@ -152,7 +150,20 @@ def _inject_signed_baggage(request, **kwargs): # register `before-sign` handler that third-party signed baggage. client.meta.events.register("before-sign", _inject_signed_baggage) - response = _head_object(client, span_streaming) + if span_streaming: + with sentry_sdk.traces.start_span( # type: ignore[attr-defined] + name="incoming" + ): + response = client.head_object( + Bucket="example-bucket", + Key="example-key", + ) + else: + with sentry_sdk.start_transaction(name="incoming", sampled=True): + response = client.head_object( + Bucket="example-bucket", + Key="example-key", + ) assert response["ResponseMetadata"]["HTTPStatusCode"] == 200 headers = _AwsRequestHandler.requests[-1] From 4ba89011728a7fca8f9d8a61d2b898e46e00460e Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Thu, 6 Aug 2026 11:57:28 +0200 Subject: [PATCH 05/37] test(stdlib): cover httplib SigV4 trace header preservation Refs: #7031 & PY-2667 --- tests/integrations/stdlib/test_httplib.py | 118 ++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/tests/integrations/stdlib/test_httplib.py b/tests/integrations/stdlib/test_httplib.py index 66d1e8db37..33aaa19aae 100644 --- a/tests/integrations/stdlib/test_httplib.py +++ b/tests/integrations/stdlib/test_httplib.py @@ -76,6 +76,43 @@ def create_chunked_server(): CHUNKED_PORT = create_chunked_server() +@pytest.fixture +def local_http_server(): + requests = [] + + class TraceHeaderHandler(BaseHTTPRequestHandler): + def do_POST(self): + requests.append(self.headers) + self.send_response(200) + self.send_header("Content-Length", "0") + self.end_headers() + + server = HTTPServer(("127.0.0.1", 0), TraceHeaderHandler) + thread = Thread(target=server.serve_forever, daemon=True) + thread.start() + + try: + yield server, requests + finally: + server.shutdown() + server.server_close() + thread.join() + + +def _request(server, headers): + connection = HTTPConnection("127.0.0.1", server.server_port) + connection.putrequest("POST", "/") + + for key, value in headers: + connection.putheader(key, value) + + connection.endheaders() + + response = connection.getresponse() + response.read() + connection.close() + + def test_crumb_capture(sentry_init, capture_events): sentry_init(integrations=[StdlibIntegration()], send_default_pii=True) events = capture_events() @@ -526,6 +563,87 @@ def getresponse(self, *args, **kwargs): assert request_headers["baggage"] == expected_outgoing_baggage +@pytest.mark.parametrize("span_streaming", [False, True]) +def test_outgoing_trace_headers_append_to_unsigned_baggage( + sentry_init, local_http_server, span_streaming +): + sentry_init( + traces_sample_rate=1.0, + trace_lifecycle="stream" if span_streaming else "static", + default_integrations=False, + integrations=[StdlibIntegration()], + ) + server, requests = local_http_server + + with mock.patch("sentry_sdk.tracing_utils.Random.randrange", return_value=500000): + if span_streaming: + with sentry_sdk.traces.start_span(name="test"): # type: ignore[attr-defined] + _request(server, [("baggage", "vendor=value")]) + else: + with sentry_sdk.start_transaction(name="test", sampled=True): + _request(server, [("baggage", "vendor=value")]) + + headers = requests[0] + + baggage_headers = headers.get_all("baggage") + assert baggage_headers is not None + # preserve existing unsigned baggage + assert len(baggage_headers) == 2 + assert baggage_headers[0] == "vendor=value" + assert baggage_headers[1].count("sentry-trace_id=") == 1 + assert "sentry-sample_rand=0.500000" in baggage_headers[1] + assert len(headers.get_all("sentry-trace")) == 1 + + +@pytest.mark.parametrize("span_streaming", [False, True]) +def test_outgoing_trace_headers_skip_signed_baggage( + sentry_init, local_http_server, span_streaming +): + sentry_init( + traces_sample_rate=1.0, + trace_lifecycle="stream" if span_streaming else "static", + default_integrations=False, + integrations=[StdlibIntegration()], + ) + server, requests = local_http_server + + # simulate AWS SigV4 request that is already signed. + authorization = ( + "AWS4-HMAC-SHA256 " + "Credential=test/20260804/eu-west-1/secretsmanager/aws4_request, " + "SignedHeaders=baggage;host;sentry-trace, " + "Signature=sixtyseven" + ) + + if span_streaming: + with sentry_sdk.traces.start_span(name="test"): # type: ignore[attr-defined] + _request( + server, + [ + ("baggage", "vendor=value"), + ("sentry-trace", "existing-trace"), + ("Authorization", authorization), + ], + ) + else: + with sentry_sdk.start_transaction(name="test", sampled=True): + _request( + server, + [ + ("baggage", "vendor=value"), + ("sentry-trace", "existing-trace"), + ("Authorization", authorization), + ], + ) + + headers = requests[0] + + # do not append baggage after SigV4 signs it. + assert headers.get_all("baggage") == ["vendor=value"] + # preserves existing `sentry-trace` header. + assert headers.get_all("sentry-trace") == ["existing-trace"] + + @pytest.mark.parametrize( "trace_propagation_targets,host,path,trace_propagated", [ From 92672ca0ae35daad1e17f6e734fda230a1564dad Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Thu, 6 Aug 2026 12:28:05 +0200 Subject: [PATCH 06/37] test(boto3): Add `server_close()` in test cleanup # Refs: #7031 & PY-2667 --- tests/integrations/boto3/test_trace_propagation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/integrations/boto3/test_trace_propagation.py b/tests/integrations/boto3/test_trace_propagation.py index e31edfeb06..39477b1b42 100644 --- a/tests/integrations/boto3/test_trace_propagation.py +++ b/tests/integrations/boto3/test_trace_propagation.py @@ -120,6 +120,7 @@ def capture_headers_after_instrumentation(request, **kwargs): assert "sentry-trace" in signed_headers.split(";") finally: server.shutdown() + server.server_close() thread.join() @@ -173,6 +174,7 @@ def _inject_signed_baggage(request, **kwargs): assert len(headers.get_all("sentry-trace")) == 1 finally: server.shutdown() + server.server_close() thread.join() From 37fa9ff908344b59113f9ec2b837b83dc85e7c0e Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Thu, 6 Aug 2026 14:41:00 +0200 Subject: [PATCH 07/37] fix(stdlib): clarify comment Refs: #7031 & PY-2667 --- sentry_sdk/integrations/stdlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index e7c1080626..fd1ea6dec1 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -225,7 +225,7 @@ def endheaders(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": for header_name, header_value in trace_headers: normalized_header = header_name.lower() - # do not mutate headers already signed by SigV4. + # preserve signed headers and avoid duplicate `sentry-trace`. if normalized_header in existing_headers and ( normalized_header != BAGGAGE_HEADER_NAME or normalized_header in signed_headers From e675673bce8993e2f348f27f2b93d2d7f299a70f Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Thu, 6 Aug 2026 14:49:55 +0200 Subject: [PATCH 08/37] fix(stdlib): Move trace header generation into `endheaders()` Refs: #7031 & PY-2667 --- sentry_sdk/integrations/stdlib.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index fd1ea6dec1..2e4faf81da 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -196,17 +196,12 @@ def putrequest( try: rv = real_putrequest(self, method, url, *args, **kwargs) except BaseException: - self._sentrysdk_trace_headers = () # type: ignore[attr-defined] self._sentrysdk_trace_url = None # type: ignore[attr-defined] raise if should_propagate_trace(client, real_url): - self._sentrysdk_trace_headers = tuple( # type: ignore[attr-defined] - sentry_sdk.get_current_scope().iter_trace_propagation_headers(span=span) - ) self._sentrysdk_trace_url = real_url # type: ignore[attr-defined] else: - self._sentrysdk_trace_headers = () # type: ignore[attr-defined] self._sentrysdk_trace_url = None # type: ignore[attr-defined] self._sentrysdk_span = span # type: ignore[attr-defined] @@ -214,16 +209,21 @@ def putrequest( return rv def endheaders(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": - trace_headers = getattr(self, "_sentrysdk_trace_headers", ()) real_url = getattr(self, "_sentrysdk_trace_url", None) + span = getattr(self, "_sentrysdk_span", None) try: - if trace_headers: + if real_url is not None: request_buffer = getattr(self, "_buffer", None) existing_headers = _request_header_names(request_buffer) signed_headers = _aws_sigv4_signed_headers(request_buffer) - for header_name, header_value in trace_headers: + for ( + header_name, + header_value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( + span=span + ): normalized_header = header_name.lower() # preserve signed headers and avoid duplicate `sentry-trace`. if normalized_header in existing_headers and ( @@ -241,7 +241,6 @@ def endheaders(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": return real_endheaders(self, *args, **kwargs) finally: - self._sentrysdk_trace_headers = () # type: ignore[attr-defined] self._sentrysdk_trace_url = None # type: ignore[attr-defined] def getresponse(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": From 14bad29b2c9730d635b4d962dfc459a0aed1f195 Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Thu, 6 Aug 2026 16:22:33 +0200 Subject: [PATCH 09/37] fix(stdlib): Replace `return set()` with `continue` to avoid skipping SigV4 headers Refs: #7031 & PY-2667 --- sentry_sdk/integrations/stdlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index 2e4faf81da..91106eca74 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -71,7 +71,7 @@ def _aws_sigv4_signed_headers(buffer: "Optional[List[bytes]]") -> "Set[str]": value = value.lstrip() if not value.startswith((b"AWS4-HMAC-SHA256", b"AWS4-ECDSA-P256-SHA256")): - return set() + continue for part in value.split(b","): part = part.strip() From 90a77d633f1cfeef53568d2cc3fa212bd0260f91 Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Tue, 11 Aug 2026 13:13:05 +0200 Subject: [PATCH 10/37] fix(stdlib): Move `get_aws_sigv4_signed_headers` to `sentry_sdk.utils` + support for SigV4 query/presigned authentication Refs: #7031 & PY-2667 --- sentry_sdk/integrations/stdlib.py | 27 ++-------------- sentry_sdk/utils.py | 54 +++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 25 deletions(-) diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index 91106eca74..f88f010a58 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -21,6 +21,7 @@ SENSITIVE_DATA_SUBSTITUTE, capture_internal_exceptions, ensure_integration_enabled, + get_aws_sigv4_signed_headers, is_sentry_url, logger, parse_url, @@ -61,30 +62,6 @@ def add_python_runtime_context( return event -def _aws_sigv4_signed_headers(buffer: "Optional[List[bytes]]") -> "Set[str]": - if buffer is None: - return set() - for line in buffer: - name, separator, value = line.partition(b":") - if not separator or name.lower() != b"authorization": - continue - - value = value.lstrip() - if not value.startswith((b"AWS4-HMAC-SHA256", b"AWS4-ECDSA-P256-SHA256")): - continue - - for part in value.split(b","): - part = part.strip() - if part.startswith(b"SignedHeaders="): - _, _, header_names = part.partition(b"=") - return { - header.decode("ascii", "ignore").lower() - for header in header_names.split(b";") - if header - } - return set() - - def _request_header_names(buffer: "Optional[List[bytes]]") -> "Set[str]": if buffer is None: return set() @@ -216,7 +193,7 @@ def endheaders(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": if real_url is not None: request_buffer = getattr(self, "_buffer", None) existing_headers = _request_header_names(request_buffer) - signed_headers = _aws_sigv4_signed_headers(request_buffer) + signed_headers = get_aws_sigv4_signed_headers(request_buffer, real_url) for ( header_name, diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index a6ece4faf1..d1236bf83f 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1697,6 +1697,60 @@ def parse_url(url: str, sanitize: bool = True) -> "ParsedUrl": ) +def get_aws_sigv4_signed_headers( + headers: "Any", url: "Optional[str]" = None +) -> "Set[str]": + # httpConnection exposes buffer, aiohttp uses header mapping. + if isinstance(headers, (str, bytes)): + authorization = headers + elif headers is None: + authorization = "" + elif hasattr(headers, "get"): + authorization = headers.get("Authorization", "") + else: + authorization = "" + for line in headers: + name, separator, value = line.partition(b":") + if separator and name.lower() == b"authorization": + authorization = value + break + + if isinstance(authorization, bytes): + authorization = authorization.decode("ascii", "ignore") + + signed_headers = set() + if isinstance(authorization, str): + # only AWS SigV4 authorization has the SignedHeaders parameter. + value = authorization.lstrip() + if value.startswith(("AWS4-HMAC-SHA256", "AWS4-ECDSA-P256-SHA256")): + for part in value.split(","): + part = part.strip() + if part.startswith("SignedHeaders="): + _, _, header_names = part.partition("=") + signed_headers.update( + header.lower() for header in header_names.split(";") if header + ) + break + + if url is None: + return signed_headers + + query = { + key.lower(): values for key, values in parse_qs(urlsplit(url).query).items() + } + algorithm = query.get("x-amz-algorithm", [""])[0] + if algorithm not in ("AWS4-HMAC-SHA256", "AWS4-ECDSA-P256-SHA256"): + return signed_headers + + # presigned requests have SignedHeaders in the URL query. + signed_headers.update( + header.lower() + for header in query.get("x-amz-signedheaders", [""])[0].split(";") + if header + ) + return signed_headers + + def is_valid_sample_rate(rate: "Any", source: str) -> bool: """ Checks the given sample rate to make sure it is valid type and value (a From f2d0ceeaeaa882988d6cb58181aa39b9d11be676 Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Tue, 11 Aug 2026 13:16:06 +0200 Subject: [PATCH 11/37] fix(stdlib): Make mypy happy Refs: #7031 & PY-2667 --- sentry_sdk/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index d1236bf83f..fb21ddfe86 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1718,7 +1718,7 @@ def get_aws_sigv4_signed_headers( if isinstance(authorization, bytes): authorization = authorization.decode("ascii", "ignore") - signed_headers = set() + signed_headers: "Set[str]" = set() if isinstance(authorization, str): # only AWS SigV4 authorization has the SignedHeaders parameter. value = authorization.lstrip() From e6e9e5e9a21a1836ee8e1a57a8f92b5616ed586c Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Tue, 11 Aug 2026 13:21:47 +0200 Subject: [PATCH 12/37] fix(stdlib): Capture internal exceptions in `endheaders()` Refs: #7031 & PY-2667 --- sentry_sdk/integrations/stdlib.py | 48 ++++++++++++++++--------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index f88f010a58..3f5fff7a01 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -191,31 +191,33 @@ def endheaders(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": try: if real_url is not None: - request_buffer = getattr(self, "_buffer", None) - existing_headers = _request_header_names(request_buffer) - signed_headers = get_aws_sigv4_signed_headers(request_buffer, real_url) - - for ( - header_name, - header_value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( - span=span - ): - normalized_header = header_name.lower() - # preserve signed headers and avoid duplicate `sentry-trace`. - if normalized_header in existing_headers and ( - normalized_header != BAGGAGE_HEADER_NAME - or normalized_header in signed_headers - ): - continue - - logger.debug( - "[Tracing] Adding `{key}` header {value} to outgoing request to {real_url}.".format( - key=header_name, value=header_value, real_url=real_url - ) + with capture_internal_exceptions(): + request_buffer = getattr(self, "_buffer", None) + existing_headers = _request_header_names(request_buffer) + signed_headers = get_aws_sigv4_signed_headers( + request_buffer, real_url ) - self.putheader(header_name, header_value) + for ( + header_name, + header_value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( + span=span + ): + normalized_header = header_name.lower() + # preserve signed headers and avoid duplicate `sentry-trace`. + if normalized_header in existing_headers and ( + normalized_header != BAGGAGE_HEADER_NAME + or normalized_header in signed_headers + ): + continue + + logger.debug( + "[Tracing] Adding `{key}` header {value} to outgoing request to {real_url}.".format( + key=header_name, value=header_value, real_url=real_url + ) + ) + self.putheader(header_name, header_value) return real_endheaders(self, *args, **kwargs) finally: self._sentrysdk_trace_url = None # type: ignore[attr-defined] From 3f4dacccf082f242b44cf9b025ce86b2e5e6593c Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Tue, 11 Aug 2026 14:31:22 +0200 Subject: [PATCH 13/37] test(utils): Add test for `get_aws_sigv4_signed_headers` Refs: #7031 & PY-2667 --- tests/test_utils.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_utils.py b/tests/test_utils.py index 64973ea5dd..716095c6f8 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -18,6 +18,7 @@ env_to_bool, exc_info_from_error, format_timestamp, + get_aws_sigv4_signed_headers, get_current_thread_meta, get_default_release, get_error_message, @@ -665,6 +666,35 @@ def test_default_release_empty_string(): assert release is None +@pytest.mark.parametrize( + "headers,url,expected", + [ + ( + { + "Authorization": ( + "AWS4-HMAC-SHA256 " + "Credential=test/20260804/eu-west-1/secretsmanager/aws4_request, " + "SignedHeaders=Host;X-Amz-Date, " + "Signature=sixtyseven" + ) + }, + None, + {"host", "x-amz-date"}, + ), + ( + {}, + "https://example.com/?" + "X-Amz-Algorithm=AWS4-HMAC-SHA256&" + "X-Amz-SignedHeaders=host%3Bx-amz-date&" + "X-Amz-Signature=sixtyseven", + {"host", "x-amz-date"}, + ), + ], +) +def test_get_aws_sigv4_signed_headers(headers, url, expected): + assert get_aws_sigv4_signed_headers(headers, url) == expected + + def test_get_default_release_sentry_release_env(monkeypatch): monkeypatch.setenv("SENTRY_RELEASE", "sentry-env-release") assert get_default_release() == "sentry-env-release" From 6d7301c8926a76bbbe8ea9dddd92468910df27c3 Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Tue, 11 Aug 2026 15:08:11 +0200 Subject: [PATCH 14/37] fix(aiohttp): Preserve SigV4 signed headers Refs: #7031 & PY-2667 --- sentry_sdk/integrations/aiohttp.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py index 858bf273f2..a0ccd9c59b 100644 --- a/sentry_sdk/integrations/aiohttp.py +++ b/sentry_sdk/integrations/aiohttp.py @@ -49,6 +49,7 @@ capture_internal_exceptions, ensure_integration_enabled, event_from_exception, + get_aws_sigv4_signed_headers, has_data_collection_enabled, logger, parse_url, @@ -456,12 +457,19 @@ async def on_request_start( span = legacy_span if should_propagate_trace(client, str(params.url)): + signed_headers = get_aws_sigv4_signed_headers( + params.headers, str(params.url) + ) for ( key, value, ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( span=span ): + # do not modify a header whose value is already signed. + if key.lower() in signed_headers: + continue + logger.debug( "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format( key=key, value=value, url=params.url From a0c1ad2a757d777f4c7fe49d1db4ad255e753199 Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Tue, 11 Aug 2026 15:08:37 +0200 Subject: [PATCH 15/37] test(aiohttp): Add tests related to SigV4 issues Refs: #7031 & PY-2667 --- tests/integrations/aiohttp/test_aiohttp.py | 188 ++++++++++++++++++++- 1 file changed, 187 insertions(+), 1 deletion(-) diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py index f70964e6dd..dcbf3ad42a 100644 --- a/tests/integrations/aiohttp/test_aiohttp.py +++ b/tests/integrations/aiohttp/test_aiohttp.py @@ -24,7 +24,7 @@ AioHttpIntegration, create_trace_config, ) -from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE +from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE, get_aws_sigv4_signed_headers from tests.conftest import ApproxDict from tests.integrations.utils import DATA_COLLECTION_USER_INFO_CASES @@ -640,6 +640,192 @@ async def handler(request): ) +@pytest.mark.asyncio +@pytest.mark.parametrize("span_streaming", [False, True]) +async def test_outgoing_trace_headers_preserve_signed_headers( + sentry_init, aiohttp_raw_server, aiohttp_client, span_streaming +): + sentry_init( + integrations=[AioHttpIntegration()], + traces_sample_rate=1.0, + trace_lifecycle="stream" if span_streaming else "static", + ) + + received_headers = [] + + async def handler(request): + received_headers.append(request.headers) + return web.Response(text="OK") + + raw_server = await aiohttp_raw_server(handler) + authorization = ( + "AWS4-HMAC-SHA256 " + "Credential=test/20260804/eu-west-1/secretsmanager/aws4_request, " + "SignedHeaders=baggage;host;sentry-trace, " + "Signature=sixtyseven" + ) + + client = await aiohttp_client(raw_server) + + if span_streaming: + with sentry_sdk.traces.start_span(name="test"): # type: ignore[attr-defined] + response = await client.get( + "/", + headers={ + "Authorization": authorization, + "baggage": "vendor=value", + "sentry-trace": "existing-trace", + }, + ) + else: + with sentry_sdk.start_transaction(name="test", sampled=True): + response = await client.get( + "/", + headers={ + "Authorization": authorization, + "baggage": "vendor=value", + "sentry-trace": "existing-trace", + }, + ) + + request_headers = received_headers[0] + + assert response.status == 200 + # signed `baggage` and `sentry-trace` headers are preserved. + assert len(request_headers.getall("baggage")) == 1 + assert request_headers["baggage"] == "vendor=value" + assert len(request_headers.getall("sentry-trace")) == 1 + assert request_headers["sentry-trace"] == "existing-trace" + assert get_aws_sigv4_signed_headers(headers=request_headers) >= { + "host", + "baggage", + "sentry-trace", + } + + +@pytest.mark.asyncio +@pytest.mark.parametrize("span_streaming", [False, True]) +async def test_outgoing_trace_headers_add_unsigned_headers( + sentry_init, aiohttp_raw_server, aiohttp_client, span_streaming +): + sentry_init( + integrations=[AioHttpIntegration()], + traces_sample_rate=1.0, + trace_lifecycle="stream" if span_streaming else "static", + ) + + received_headers = [] + + async def handler(request): + received_headers.append(request.headers.copy()) + return web.Response(text="OK") + + raw_server = await aiohttp_raw_server(handler) + authorization = ( + "AWS4-HMAC-SHA256 " + "Credential=test/20260804/eu-west-1/secretsmanager/aws4_request, " + "SignedHeaders=host;x-amz-date," + "Signature=sixtyseven" + ) + + client = await aiohttp_client(raw_server) + + if span_streaming: + with sentry_sdk.traces.start_span(name="test"): # type: ignore[attr-defined] + response = await client.get( + "/", + headers={ + "Authorization": authorization, + "baggage": "vendor=value", + "sentry-trace": "existing-trace", + }, + ) + else: + with sentry_sdk.start_transaction(name="test", sampled=True): + response = await client.get( + "/", + headers={ + "Authorization": authorization, + "baggage": "vendor=value", + "sentry-trace": "existing-trace", + }, + ) + request_headers = received_headers[0] + + assert response.status == 200 + # unsigned `baggage` and `sentry-trace` headers are injected. + assert len(request_headers.getall("sentry-trace")) == 1 + assert request_headers["sentry-trace"] != "existing-trace" + assert len(request_headers.getall("baggage")) == 1 + assert request_headers["baggage"].startswith("vendor=value,") + assert request_headers["baggage"].count("sentry-trace_id=") == 1 + # added `baggage` and `sentry-trace` are not in the signed header set. + assert get_aws_sigv4_signed_headers(headers=request_headers) == { + "host", + "x-amz-date", + } + + +@pytest.mark.asyncio +@pytest.mark.parametrize("span_streaming", [False, True]) +async def test_outgoing_trace_headers_skip_query_signed_baggage( + sentry_init, aiohttp_raw_server, aiohttp_client, span_streaming +): + sentry_init( + integrations=[AioHttpIntegration()], + traces_sample_rate=1.0, + trace_lifecycle="stream" if span_streaming else "static", + default_integrations=False, + ) + + received_headers = [] + received_urls = [] + + async def handler(request): + received_headers.append(request.headers.copy()) + received_urls.append(str(request.url)) + return web.Response(text="OK") + + raw_server = await aiohttp_raw_server(handler) + path = ( + "/" + "?X-Amz-Algorithm=AWS4-HMAC-SHA256" + "&X-Amz-Credential=" + "test%2F20260804%2Feu-west-1%2Fs3%2Faws4_request" + "&X-Amz-Date=20260804T120000Z" + "&X-Amz-Expires=60" + "&X-Amz-SignedHeaders=baggage%3Bhost" + "&X-Amz-Signature=sixtyseven" + ) + + client = await aiohttp_client(raw_server) + + if span_streaming: + with sentry_sdk.traces.start_span(name="test"): # type: ignore[attr-defined] + response = await client.get( + path, + headers={"baggage": "vendor=value"}, + ) + else: + with sentry_sdk.start_transaction(name="test", sampled=True): + response = await client.get( + path, + headers={"baggage": "vendor=value"}, + ) + + headers = received_headers[0] + assert response.status == 200 + # `baggage` is part of X-Amz-SignedHeaders, so it must not be modified. + assert len(headers.getall("baggage")) == 1 + assert headers["baggage"] == "vendor=value" + # `sentry-trace` was not signed, so it can be propagated. + assert len(headers.getall("sentry-trace")) == 1 + assert get_aws_sigv4_signed_headers(headers=headers, url=received_urls[0]) == { + "baggage", + "host", + } + + @pytest.mark.asyncio async def test_request_source_disabled( sentry_init, From e40e9318989b968923a094b8ead039f24e8b1e20 Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Tue, 11 Aug 2026 15:09:06 +0200 Subject: [PATCH 16/37] test(boto3): Add signed header assertions to tests Refs: #7031 & PY-2667 --- tests/integrations/boto3/test_trace_propagation.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/integrations/boto3/test_trace_propagation.py b/tests/integrations/boto3/test_trace_propagation.py index 39477b1b42..c97dcf5ea4 100644 --- a/tests/integrations/boto3/test_trace_propagation.py +++ b/tests/integrations/boto3/test_trace_propagation.py @@ -9,6 +9,7 @@ import sentry_sdk from sentry_sdk.integrations.boto3 import Boto3Integration from sentry_sdk.integrations.stdlib import StdlibIntegration +from sentry_sdk.utils import get_aws_sigv4_signed_headers class _AwsRequestHandler(BaseHTTPRequestHandler): @@ -112,12 +113,9 @@ def capture_headers_after_instrumentation(request, **kwargs): assert sentry_trace_headers is not None assert len(sentry_trace_headers) == 1 assert sentry_trace_headers == signed_request_headers["sentry-trace"] - - authorization = headers["Authorization"] - signed_headers = authorization.split("SignedHeaders=", 1)[1].split(",", 1)[0] # both `baggage` and `sentry-trace` are signed. - assert "baggage" in signed_headers.split(";") - assert "sentry-trace" in signed_headers.split(";") + signed_headers = get_aws_sigv4_signed_headers(headers=headers) + assert signed_headers >= {"baggage", "sentry-trace"} finally: server.shutdown() server.server_close() @@ -172,6 +170,9 @@ def _inject_signed_baggage(request, **kwargs): assert headers.get_all("baggage") == ["vendor=value"] # `httplib` still adds single `sentry-trace` header. assert len(headers.get_all("sentry-trace")) == 1 + signed_headers = get_aws_sigv4_signed_headers(headers=headers) + assert "baggage" in signed_headers + assert "sentry-trace" not in signed_headers finally: server.shutdown() server.server_close() @@ -200,6 +201,7 @@ def test_presigned_urls_do_not_require_sentry_headers(sentry_init): # only `host` header is signed. assert query["X-Amz-SignedHeaders"] == ["host"] + assert get_aws_sigv4_signed_headers(headers={}, url=url) == {"host"} # no `sentry-*` or baggage are added. assert "sentry-trace" not in url assert "baggage" not in url From af905db4a21031a4e6fda75e847b396e8d210b6d Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Tue, 11 Aug 2026 15:09:50 +0200 Subject: [PATCH 17/37] test(httplib): Add tests for SigV4 signed headers Refs: #7031 & PY-2667 --- tests/integrations/stdlib/test_httplib.py | 64 +++++++++++++++++++++-- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/tests/integrations/stdlib/test_httplib.py b/tests/integrations/stdlib/test_httplib.py index 33aaa19aae..04eb404f75 100644 --- a/tests/integrations/stdlib/test_httplib.py +++ b/tests/integrations/stdlib/test_httplib.py @@ -16,6 +16,7 @@ from sentry_sdk import capture_message, continue_trace, start_transaction from sentry_sdk.consts import MATCH_ALL, SPANDATA from sentry_sdk.integrations.stdlib import StdlibIntegration +from sentry_sdk.utils import get_aws_sigv4_signed_headers from tests.conftest import ApproxDict, create_mock_http_server, get_free_port PORT = create_mock_http_server() @@ -99,9 +100,9 @@ def do_POST(self): thread.join() -def _request(server, headers): +def _request(server, headers, path="/"): connection = HTTPConnection("127.0.0.1", server.server_port) - connection.putrequest("POST", "/") + connection.putrequest("POST", path) for key, value in headers: connection.putheader(key, value) @@ -575,7 +576,7 @@ def test_outgoing_trace_headers_append_to_unsigned_baggage( ) server, requests = local_http_server - with mock.patch("sentry_sdk.tracing_utils.Random.randrange", return_value=500000): + with mock.patch("sentry_sdk.tracing_utils.Random.randrange", return_value=67): if span_streaming: with sentry_sdk.traces.start_span(name="test"): # type: ignore[attr-defined] _request(server, [("baggage", "vendor=value")]) @@ -585,13 +586,13 @@ def test_outgoing_trace_headers_append_to_unsigned_baggage( headers = requests[0] + # preserve existing unsigned baggage baggage_headers = headers.get_all("baggage") assert baggage_headers is not None - # preserve existing unsigned baggage assert len(baggage_headers) == 2 assert baggage_headers[0] == "vendor=value" assert baggage_headers[1].count("sentry-trace_id=") == 1 - assert "sentry-sample_rand=0.500000" in baggage_headers[1] + assert "sentry-sample_rand=0.000067" in baggage_headers[1] assert len(headers.get_all("sentry-trace")) == 1 @@ -642,6 +643,59 @@ def test_outgoing_trace_headers_skip_signed_baggage( assert headers.get_all("baggage") == ["vendor=value"] # preserves existing `sentry-trace` header. assert headers.get_all("sentry-trace") == ["existing-trace"] + assert get_aws_sigv4_signed_headers(headers=headers) >= { + "baggage", + "host", + "sentry-trace", + } + + +@pytest.mark.parametrize("span_streaming", [False, True]) +def test_outgoing_trace_headers_skip_query_signed_baggage( + sentry_init, local_http_server, span_streaming +): + sentry_init( + traces_sample_rate=1.0, + trace_lifecycle="stream" if span_streaming else "static", + default_integrations=False, + integrations=[StdlibIntegration()], + ) + server, requests = local_http_server + path = ( + "/" + "?X-Amz-Algorithm=AWS4-HMAC-SHA256" + "&X-Amz-Credential=" + "test%2F20260804%2Feu-west-1%2Fs3%2Faws4_request" + "&X-Amz-Date=20260804T120000Z" + "&X-Amz-Expires=60" + "&X-Amz-SignedHeaders=baggage%3Bhost" + "&X-Amz-Signature=sixtyseven" + ) + + if span_streaming: + with sentry_sdk.traces.start_span(name="test"): # type: ignore[attr-defined] + _request( + server, + [("baggage", "vendor=value")], + path=path, + ) + else: + with sentry_sdk.start_transaction(name="test", sampled=True): + _request( + server, + [("baggage", "vendor=value")], + path=path, + ) + + headers = requests[0] + # `baggage` is part of X-Amz-SignedHeaders, so may not be modified. + assert len(headers.get_all("baggage")) == 1 + assert headers["baggage"] == "vendor=value" + # `sentry-trace` was not signed, so it can be propagated. + assert len(headers.get_all("sentry-trace")) == 1 + assert get_aws_sigv4_signed_headers( + headers=headers, url=f"http://127.0.0.1:{server.server_port}{path}" + ) >= {"baggage", "host"} @pytest.mark.parametrize( From 09f8c44dddc78d428a5f261a3e6b6d676a629246 Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Tue, 11 Aug 2026 15:33:13 +0200 Subject: [PATCH 18/37] fix(aiohttp): Do `request.rel_url` instead of `request.url` so py3.7-3.9 tests succeed Refs: #7031 & PY-2667 --- tests/integrations/aiohttp/test_aiohttp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py index dcbf3ad42a..28fd424b9a 100644 --- a/tests/integrations/aiohttp/test_aiohttp.py +++ b/tests/integrations/aiohttp/test_aiohttp.py @@ -783,7 +783,7 @@ async def test_outgoing_trace_headers_skip_query_signed_baggage( async def handler(request): received_headers.append(request.headers.copy()) - received_urls.append(str(request.url)) + received_urls.append(str(request.rel_url)) return web.Response(text="OK") raw_server = await aiohttp_raw_server(handler) From e07978e4ac923abfa60b9b57d7078cd76cb99bc2 Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Tue, 11 Aug 2026 16:18:32 +0200 Subject: [PATCH 19/37] fix(aiohttp): Use `raw_path` instead of `rel_url` to avoid encoding issues Refs: #7031 & PY-2667 --- tests/integrations/aiohttp/test_aiohttp.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py index 28fd424b9a..01519ed1cd 100644 --- a/tests/integrations/aiohttp/test_aiohttp.py +++ b/tests/integrations/aiohttp/test_aiohttp.py @@ -783,7 +783,9 @@ async def test_outgoing_trace_headers_skip_query_signed_baggage( async def handler(request): received_headers.append(request.headers.copy()) - received_urls.append(str(request.rel_url)) + # Use the raw target so aiohttp/yarl URL canonicalization does not + # change the encoding of the presigned query parameters. + received_urls.append(request.raw_path) return web.Response(text="OK") raw_server = await aiohttp_raw_server(handler) From 08dc4d2527c463aed67efcc4676c1a3ce1f51f90 Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Wed, 12 Aug 2026 11:12:25 +0200 Subject: [PATCH 20/37] ref(boto3): Clarify logic in `replace_header()` --- sentry_sdk/integrations/boto3.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry_sdk/integrations/boto3.py b/sentry_sdk/integrations/boto3.py index 844ec5f518..426da69d7f 100644 --- a/sentry_sdk/integrations/boto3.py +++ b/sentry_sdk/integrations/boto3.py @@ -139,6 +139,7 @@ def _sentry_before_sign( return def _replace_header(request: "AWSRequest", key: str, value: str) -> None: + # HTTPHeaders appends on assignment, so delete existing values first. if key in request.headers: del request.headers[key] request.headers[key] = value From 079cf176194bb93c0b0523097d058e994116ffc6 Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Wed, 12 Aug 2026 11:23:35 +0200 Subject: [PATCH 21/37] ref(utils): Remove bytes parsing from `get_aws_sigv4_signed_headers()` since it's not used anymore --- sentry_sdk/utils.py | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index fb21ddfe86..90aae1c914 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -47,6 +47,7 @@ Generator, Iterator, List, + Mapping, NoReturn, Optional, ParamSpec, @@ -1698,25 +1699,9 @@ def parse_url(url: str, sanitize: bool = True) -> "ParsedUrl": def get_aws_sigv4_signed_headers( - headers: "Any", url: "Optional[str]" = None + headers: "Optional[Mapping[str, str]]", url: "Optional[str]" = None ) -> "Set[str]": - # httpConnection exposes buffer, aiohttp uses header mapping. - if isinstance(headers, (str, bytes)): - authorization = headers - elif headers is None: - authorization = "" - elif hasattr(headers, "get"): - authorization = headers.get("Authorization", "") - else: - authorization = "" - for line in headers: - name, separator, value = line.partition(b":") - if separator and name.lower() == b"authorization": - authorization = value - break - - if isinstance(authorization, bytes): - authorization = authorization.decode("ascii", "ignore") + authorization = headers.get("Authorization", "") if headers is not None else "" signed_headers: "Set[str]" = set() if isinstance(authorization, str): From ebcce0222db4df91389f5c6be1243e8636fb4e72 Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Wed, 12 Aug 2026 13:50:26 +0200 Subject: [PATCH 22/37] ref(utils): `get_aws_sigv4_signed_headers()` now takes in `authorization` string instead of all headers --- sentry_sdk/integrations/aiohttp.py | 2 +- sentry_sdk/utils.py | 5 ++--- tests/integrations/aiohttp/test_aiohttp.py | 6 +++--- .../boto3/test_trace_propagation.py | 10 +++++++--- tests/integrations/stdlib/test_httplib.py | 6 ++++-- tests/test_utils.py | 20 ++++++++----------- 6 files changed, 25 insertions(+), 24 deletions(-) diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py index a0ccd9c59b..62fe042bf9 100644 --- a/sentry_sdk/integrations/aiohttp.py +++ b/sentry_sdk/integrations/aiohttp.py @@ -458,7 +458,7 @@ async def on_request_start( if should_propagate_trace(client, str(params.url)): signed_headers = get_aws_sigv4_signed_headers( - params.headers, str(params.url) + authorization=params.headers.get("Authorization", ""), url=str(params.url) ) for ( key, diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 90aae1c914..66e7377f25 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1699,12 +1699,11 @@ def parse_url(url: str, sanitize: bool = True) -> "ParsedUrl": def get_aws_sigv4_signed_headers( - headers: "Optional[Mapping[str, str]]", url: "Optional[str]" = None + authorization: "Optional[str]", url: "Optional[str]" = None ) -> "Set[str]": - authorization = headers.get("Authorization", "") if headers is not None else "" signed_headers: "Set[str]" = set() - if isinstance(authorization, str): + if authorization is not None: # only AWS SigV4 authorization has the SignedHeaders parameter. value = authorization.lstrip() if value.startswith(("AWS4-HMAC-SHA256", "AWS4-ECDSA-P256-SHA256")): diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py index 01519ed1cd..4dfaecadd4 100644 --- a/tests/integrations/aiohttp/test_aiohttp.py +++ b/tests/integrations/aiohttp/test_aiohttp.py @@ -696,7 +696,7 @@ async def handler(request): assert request_headers["baggage"] == "vendor=value" assert len(request_headers.getall("sentry-trace")) == 1 assert request_headers["sentry-trace"] == "existing-trace" - assert get_aws_sigv4_signed_headers(headers=request_headers) >= { + assert get_aws_sigv4_signed_headers(authorization=request_headers.get("Authorization", "")) >= { "host", "baggage", "sentry-trace", @@ -760,7 +760,7 @@ async def handler(request): assert request_headers["baggage"].startswith("vendor=value,") assert request_headers["baggage"].count("sentry-trace_id=") == 1 # added `baggage` and `sentry-trace` are not in the signed header set. - assert get_aws_sigv4_signed_headers(headers=request_headers) == { + assert get_aws_sigv4_signed_headers(authorization=request_headers.get("Authorization", "")) == { "host", "x-amz-date", } @@ -822,7 +822,7 @@ async def handler(request): assert headers["baggage"] == "vendor=value" # `sentry-trace` was not signed, so it can be propagated. assert len(headers.getall("sentry-trace")) == 1 - assert get_aws_sigv4_signed_headers(headers=headers, url=received_urls[0]) == { + assert get_aws_sigv4_signed_headers(authorization=headers.get("Authorization", ""), url=received_urls[0]) == { "baggage", "host", } diff --git a/tests/integrations/boto3/test_trace_propagation.py b/tests/integrations/boto3/test_trace_propagation.py index c97dcf5ea4..147b8d23ca 100644 --- a/tests/integrations/boto3/test_trace_propagation.py +++ b/tests/integrations/boto3/test_trace_propagation.py @@ -114,7 +114,9 @@ def capture_headers_after_instrumentation(request, **kwargs): assert len(sentry_trace_headers) == 1 assert sentry_trace_headers == signed_request_headers["sentry-trace"] # both `baggage` and `sentry-trace` are signed. - signed_headers = get_aws_sigv4_signed_headers(headers=headers) + signed_headers = get_aws_sigv4_signed_headers( + authorization=headers.get("Authorization", "") + ) assert signed_headers >= {"baggage", "sentry-trace"} finally: server.shutdown() @@ -170,7 +172,9 @@ def _inject_signed_baggage(request, **kwargs): assert headers.get_all("baggage") == ["vendor=value"] # `httplib` still adds single `sentry-trace` header. assert len(headers.get_all("sentry-trace")) == 1 - signed_headers = get_aws_sigv4_signed_headers(headers=headers) + signed_headers = get_aws_sigv4_signed_headers( + authorization=headers.get("Authorization", "") + ) assert "baggage" in signed_headers assert "sentry-trace" not in signed_headers finally: @@ -201,7 +205,7 @@ def test_presigned_urls_do_not_require_sentry_headers(sentry_init): # only `host` header is signed. assert query["X-Amz-SignedHeaders"] == ["host"] - assert get_aws_sigv4_signed_headers(headers={}, url=url) == {"host"} + assert get_aws_sigv4_signed_headers(authorization=None, url=url) == {"host"} # no `sentry-*` or baggage are added. assert "sentry-trace" not in url assert "baggage" not in url diff --git a/tests/integrations/stdlib/test_httplib.py b/tests/integrations/stdlib/test_httplib.py index 04eb404f75..2dacc6b740 100644 --- a/tests/integrations/stdlib/test_httplib.py +++ b/tests/integrations/stdlib/test_httplib.py @@ -643,7 +643,9 @@ def test_outgoing_trace_headers_skip_signed_baggage( assert headers.get_all("baggage") == ["vendor=value"] # preserves existing `sentry-trace` header. assert headers.get_all("sentry-trace") == ["existing-trace"] - assert get_aws_sigv4_signed_headers(headers=headers) >= { + assert get_aws_sigv4_signed_headers( + authorization=headers.get("Authorization", "") + ) >= { "baggage", "host", "sentry-trace", @@ -694,7 +696,7 @@ def test_outgoing_trace_headers_skip_query_signed_baggage( # `sentry-trace` was not signed, so it can be propagated. assert len(headers.get_all("sentry-trace")) == 1 assert get_aws_sigv4_signed_headers( - headers=headers, url=f"http://127.0.0.1:{server.server_port}{path}" + authorization=headers.get("Authorization", ""), url=f"http://127.0.0.1:{server.server_port}{path}" ) >= {"baggage", "host"} diff --git a/tests/test_utils.py b/tests/test_utils.py index 716095c6f8..2735270222 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -667,22 +667,18 @@ def test_default_release_empty_string(): @pytest.mark.parametrize( - "headers,url,expected", + "authorization,url,expected", [ ( - { - "Authorization": ( - "AWS4-HMAC-SHA256 " - "Credential=test/20260804/eu-west-1/secretsmanager/aws4_request, " - "SignedHeaders=Host;X-Amz-Date, " - "Signature=sixtyseven" - ) - }, + "AWS4-HMAC-SHA256 " + "Credential=test/20260804/eu-west-1/secretsmanager/aws4_request, " + "SignedHeaders=Host;X-Amz-Date, " + "Signature=sixtyseven", None, {"host", "x-amz-date"}, ), ( - {}, + "", "https://example.com/?" "X-Amz-Algorithm=AWS4-HMAC-SHA256&" "X-Amz-SignedHeaders=host%3Bx-amz-date&" @@ -691,8 +687,8 @@ def test_default_release_empty_string(): ), ], ) -def test_get_aws_sigv4_signed_headers(headers, url, expected): - assert get_aws_sigv4_signed_headers(headers, url) == expected +def test_get_aws_sigv4_signed_headers(authorization, url, expected): + assert get_aws_sigv4_signed_headers(authorization=authorization, url=url) == expected def test_get_default_release_sentry_release_env(monkeypatch): From 484a941479d1ee9e32db9f0bcc79c1e1f9ba4c0c Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Wed, 12 Aug 2026 13:55:31 +0200 Subject: [PATCH 23/37] chore(stdlib): Track headers during request construction - Record existing and signed headers in `putheader()` so trace propagation can avoid reparsing `_buffer` on every request. Refs: #7031 & PY-2667 --- sentry_sdk/integrations/stdlib.py | 60 +++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index 3f5fff7a01..84025d212a 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -62,17 +62,6 @@ def add_python_runtime_context( return event -def _request_header_names(buffer: "Optional[List[bytes]]") -> "Set[str]": - if buffer is None: - return set() - names = set() - for line in buffer: - name, separator, _ = line.partition(b":") - if separator: - names.add(name.decode("ascii", "ignore").lower()) - return names - - def _complete_span(span: "Union[Span, StreamedSpan]") -> None: if isinstance(span, StreamedSpan): with capture_internal_exceptions(): @@ -86,11 +75,38 @@ def _complete_span(span: "Union[Span, StreamedSpan]") -> None: def _install_httplib() -> None: real_putrequest = HTTPConnection.putrequest + real_putheader = HTTPConnection.putheader real_endheaders = HTTPConnection.endheaders real_getresponse = HTTPConnection.getresponse real_read = HTTPResponse.read real_close = HTTPResponse.close + def putheader(self: "HTTPConnection", header: "Any", *values: "Any") -> "Any": + rv = real_putheader(self, header, *values) + + request_header_names = getattr(self, "_sentrysdk_request_headers", None) + if request_header_names is None: + return rv + + if isinstance(header, bytes): + normalized_header = header.decode("ascii", "ignore").lower() + elif isinstance(header, str): + normalized_header = header.lower() + else: + return rv + + request_header_names.add(normalized_header) + if normalized_header == "authorization" and values: + signed_headers = getattr(self, "_sentrysdk_signed_headers", None) + if signed_headers is not None: + with capture_internal_exceptions(): + authorization = values[0] + if isinstance(authorization, bytes): + authorization = authorization.decode("ascii", "ignore") + signed_headers.update(get_aws_sigv4_signed_headers(authorization)) + + return rv + def putrequest( self: "HTTPConnection", method: str, url: str, *args: "Any", **kwargs: "Any" ) -> "Any": @@ -170,10 +186,17 @@ def putrequest( set_on_span(SPANDATA.NETWORK_PEER_ADDRESS, self.host) set_on_span(SPANDATA.NETWORK_PEER_PORT, self.port) + # track existing headers separately from signed so thatexisting unsigned + # baggage header can be extended while signed baggage stays unchanged. + self._sentrysdk_request_headers = set() # type: ignore[attr-defined] + self._sentrysdk_signed_headers = set() # type: ignore[attr-defined] + try: rv = real_putrequest(self, method, url, *args, **kwargs) except BaseException: self._sentrysdk_trace_url = None # type: ignore[attr-defined] + self._sentrysdk_request_headers = None # type: ignore[attr-defined] + self._sentrysdk_signed_headers = None # type: ignore[attr-defined] raise if should_propagate_trace(client, real_url): @@ -192,10 +215,14 @@ def endheaders(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": try: if real_url is not None: with capture_internal_exceptions(): - request_buffer = getattr(self, "_buffer", None) - existing_headers = _request_header_names(request_buffer) - signed_headers = get_aws_sigv4_signed_headers( - request_buffer, real_url + existing_headers: "Set[str]" = getattr( + self, "_sentrysdk_request_headers", set() + ) + signed_headers: "Set[str]" = getattr( + self, "_sentrysdk_signed_headers", set() + ) + signed_headers.update( + get_aws_sigv4_signed_headers(authorization=None, url=real_url) ) for ( @@ -221,6 +248,8 @@ def endheaders(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": return real_endheaders(self, *args, **kwargs) finally: self._sentrysdk_trace_url = None # type: ignore[attr-defined] + self._sentrysdk_request_header_names = None # type: ignore[attr-defined] + self._sentrysdk_signed_headers = None # type: ignore[attr-defined] def getresponse(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": span = getattr(self, "_sentrysdk_span", None) @@ -277,6 +306,7 @@ def close(self: "HTTPResponse") -> None: self._sentrysdk_span = None # type: ignore[attr-defined] _complete_span(span) + HTTPConnection.putheader = putheader # type: ignore[method-assign] HTTPConnection.putrequest = putrequest # type: ignore[method-assign] HTTPConnection.endheaders = endheaders # type: ignore[method-assign] HTTPConnection.getresponse = getresponse # type: ignore[method-assign] From 35ff7b43b6adaa8dbc94563631035a2e52c0ef7f Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Wed, 12 Aug 2026 13:56:48 +0200 Subject: [PATCH 24/37] linting --- sentry_sdk/integrations/aiohttp.py | 3 ++- tests/integrations/aiohttp/test_aiohttp.py | 12 +++++++++--- tests/integrations/stdlib/test_httplib.py | 3 ++- tests/test_utils.py | 4 +++- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py index 62fe042bf9..cbff0ed60c 100644 --- a/sentry_sdk/integrations/aiohttp.py +++ b/sentry_sdk/integrations/aiohttp.py @@ -458,7 +458,8 @@ async def on_request_start( if should_propagate_trace(client, str(params.url)): signed_headers = get_aws_sigv4_signed_headers( - authorization=params.headers.get("Authorization", ""), url=str(params.url) + authorization=params.headers.get("Authorization", ""), + url=str(params.url), ) for ( key, diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py index 4dfaecadd4..913d7c563c 100644 --- a/tests/integrations/aiohttp/test_aiohttp.py +++ b/tests/integrations/aiohttp/test_aiohttp.py @@ -696,7 +696,9 @@ async def handler(request): assert request_headers["baggage"] == "vendor=value" assert len(request_headers.getall("sentry-trace")) == 1 assert request_headers["sentry-trace"] == "existing-trace" - assert get_aws_sigv4_signed_headers(authorization=request_headers.get("Authorization", "")) >= { + assert get_aws_sigv4_signed_headers( + authorization=request_headers.get("Authorization", "") + ) >= { "host", "baggage", "sentry-trace", @@ -760,7 +762,9 @@ async def handler(request): assert request_headers["baggage"].startswith("vendor=value,") assert request_headers["baggage"].count("sentry-trace_id=") == 1 # added `baggage` and `sentry-trace` are not in the signed header set. - assert get_aws_sigv4_signed_headers(authorization=request_headers.get("Authorization", "")) == { + assert get_aws_sigv4_signed_headers( + authorization=request_headers.get("Authorization", "") + ) == { "host", "x-amz-date", } @@ -822,7 +826,9 @@ async def handler(request): assert headers["baggage"] == "vendor=value" # `sentry-trace` was not signed, so it can be propagated. assert len(headers.getall("sentry-trace")) == 1 - assert get_aws_sigv4_signed_headers(authorization=headers.get("Authorization", ""), url=received_urls[0]) == { + assert get_aws_sigv4_signed_headers( + authorization=headers.get("Authorization", ""), url=received_urls[0] + ) == { "baggage", "host", } diff --git a/tests/integrations/stdlib/test_httplib.py b/tests/integrations/stdlib/test_httplib.py index 2dacc6b740..e62bc53ed5 100644 --- a/tests/integrations/stdlib/test_httplib.py +++ b/tests/integrations/stdlib/test_httplib.py @@ -696,7 +696,8 @@ def test_outgoing_trace_headers_skip_query_signed_baggage( # `sentry-trace` was not signed, so it can be propagated. assert len(headers.get_all("sentry-trace")) == 1 assert get_aws_sigv4_signed_headers( - authorization=headers.get("Authorization", ""), url=f"http://127.0.0.1:{server.server_port}{path}" + authorization=headers.get("Authorization", ""), + url=f"http://127.0.0.1:{server.server_port}{path}", ) >= {"baggage", "host"} diff --git a/tests/test_utils.py b/tests/test_utils.py index 2735270222..c28f4f322c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -688,7 +688,9 @@ def test_default_release_empty_string(): ], ) def test_get_aws_sigv4_signed_headers(authorization, url, expected): - assert get_aws_sigv4_signed_headers(authorization=authorization, url=url) == expected + assert ( + get_aws_sigv4_signed_headers(authorization=authorization, url=url) == expected + ) def test_get_default_release_sentry_release_env(monkeypatch): From 355be077eecfa9e3c8005488a9cd8f517a0aeb4b Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Wed, 12 Aug 2026 13:57:51 +0200 Subject: [PATCH 25/37] fix(stdlib): Fix renaming issue --- sentry_sdk/integrations/stdlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index 84025d212a..7e26464877 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -248,7 +248,7 @@ def endheaders(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": return real_endheaders(self, *args, **kwargs) finally: self._sentrysdk_trace_url = None # type: ignore[attr-defined] - self._sentrysdk_request_header_names = None # type: ignore[attr-defined] + self._sentrysdk_request_header = None # type: ignore[attr-defined] self._sentrysdk_signed_headers = None # type: ignore[attr-defined] def getresponse(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": From 5ab6ccb555372ca5bcd24bdb82ef4379cad7f59a Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Wed, 12 Aug 2026 14:06:09 +0200 Subject: [PATCH 26/37] ref(utils): Remove unused `Mapping` import --- sentry_sdk/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 66e7377f25..3717d94ddf 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -47,7 +47,6 @@ Generator, Iterator, List, - Mapping, NoReturn, Optional, ParamSpec, From f04bd51a73797ec1e9f5dac81417be9a0921280e Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Wed, 12 Aug 2026 15:28:58 +0200 Subject: [PATCH 27/37] ref(stdlib): Fix another typo --- sentry_sdk/integrations/stdlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index 7e26464877..2f513ecfa7 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -248,7 +248,7 @@ def endheaders(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": return real_endheaders(self, *args, **kwargs) finally: self._sentrysdk_trace_url = None # type: ignore[attr-defined] - self._sentrysdk_request_header = None # type: ignore[attr-defined] + self._sentrysdk_request_headers = None # type: ignore[attr-defined] self._sentrysdk_signed_headers = None # type: ignore[attr-defined] def getresponse(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": From 28e33cc79aff97abec64e0f29df83ed5d17ba0eb Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 24 Aug 2026 09:59:41 +0200 Subject: [PATCH 28/37] revert aiohttp changes --- sentry_sdk/integrations/aiohttp.py | 9 - tests/integrations/aiohttp/test_aiohttp.py | 196 +-------------------- 2 files changed, 1 insertion(+), 204 deletions(-) diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py index cbff0ed60c..858bf273f2 100644 --- a/sentry_sdk/integrations/aiohttp.py +++ b/sentry_sdk/integrations/aiohttp.py @@ -49,7 +49,6 @@ capture_internal_exceptions, ensure_integration_enabled, event_from_exception, - get_aws_sigv4_signed_headers, has_data_collection_enabled, logger, parse_url, @@ -457,20 +456,12 @@ async def on_request_start( span = legacy_span if should_propagate_trace(client, str(params.url)): - signed_headers = get_aws_sigv4_signed_headers( - authorization=params.headers.get("Authorization", ""), - url=str(params.url), - ) for ( key, value, ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( span=span ): - # do not modify a header whose value is already signed. - if key.lower() in signed_headers: - continue - logger.debug( "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format( key=key, value=value, url=params.url diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py index 913d7c563c..f70964e6dd 100644 --- a/tests/integrations/aiohttp/test_aiohttp.py +++ b/tests/integrations/aiohttp/test_aiohttp.py @@ -24,7 +24,7 @@ AioHttpIntegration, create_trace_config, ) -from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE, get_aws_sigv4_signed_headers +from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE from tests.conftest import ApproxDict from tests.integrations.utils import DATA_COLLECTION_USER_INFO_CASES @@ -640,200 +640,6 @@ async def handler(request): ) -@pytest.mark.asyncio -@pytest.mark.parametrize("span_streaming", [False, True]) -async def test_outgoing_trace_headers_preserve_signed_headers( - sentry_init, aiohttp_raw_server, aiohttp_client, span_streaming -): - sentry_init( - integrations=[AioHttpIntegration()], - traces_sample_rate=1.0, - trace_lifecycle="stream" if span_streaming else "static", - ) - - received_headers = [] - - async def handler(request): - received_headers.append(request.headers) - return web.Response(text="OK") - - raw_server = await aiohttp_raw_server(handler) - authorization = ( - "AWS4-HMAC-SHA256 " - "Credential=test/20260804/eu-west-1/secretsmanager/aws4_request, " - "SignedHeaders=baggage;host;sentry-trace, " - "Signature=sixtyseven" - ) - - client = await aiohttp_client(raw_server) - - if span_streaming: - with sentry_sdk.traces.start_span(name="test"): # type: ignore[attr-defined] - response = await client.get( - "/", - headers={ - "Authorization": authorization, - "baggage": "vendor=value", - "sentry-trace": "existing-trace", - }, - ) - else: - with sentry_sdk.start_transaction(name="test", sampled=True): - response = await client.get( - "/", - headers={ - "Authorization": authorization, - "baggage": "vendor=value", - "sentry-trace": "existing-trace", - }, - ) - - request_headers = received_headers[0] - - assert response.status == 200 - # signed `baggage` and `sentry-trace` headers are preserved. - assert len(request_headers.getall("baggage")) == 1 - assert request_headers["baggage"] == "vendor=value" - assert len(request_headers.getall("sentry-trace")) == 1 - assert request_headers["sentry-trace"] == "existing-trace" - assert get_aws_sigv4_signed_headers( - authorization=request_headers.get("Authorization", "") - ) >= { - "host", - "baggage", - "sentry-trace", - } - - -@pytest.mark.asyncio -@pytest.mark.parametrize("span_streaming", [False, True]) -async def test_outgoing_trace_headers_add_unsigned_headers( - sentry_init, aiohttp_raw_server, aiohttp_client, span_streaming -): - sentry_init( - integrations=[AioHttpIntegration()], - traces_sample_rate=1.0, - trace_lifecycle="stream" if span_streaming else "static", - ) - - received_headers = [] - - async def handler(request): - received_headers.append(request.headers.copy()) - return web.Response(text="OK") - - raw_server = await aiohttp_raw_server(handler) - authorization = ( - "AWS4-HMAC-SHA256 " - "Credential=test/20260804/eu-west-1/secretsmanager/aws4_request, " - "SignedHeaders=host;x-amz-date," - "Signature=sixtyseven" - ) - - client = await aiohttp_client(raw_server) - - if span_streaming: - with sentry_sdk.traces.start_span(name="test"): # type: ignore[attr-defined] - response = await client.get( - "/", - headers={ - "Authorization": authorization, - "baggage": "vendor=value", - "sentry-trace": "existing-trace", - }, - ) - else: - with sentry_sdk.start_transaction(name="test", sampled=True): - response = await client.get( - "/", - headers={ - "Authorization": authorization, - "baggage": "vendor=value", - "sentry-trace": "existing-trace", - }, - ) - request_headers = received_headers[0] - - assert response.status == 200 - # unsigned `baggage` and `sentry-trace` headers are injected. - assert len(request_headers.getall("sentry-trace")) == 1 - assert request_headers["sentry-trace"] != "existing-trace" - assert len(request_headers.getall("baggage")) == 1 - assert request_headers["baggage"].startswith("vendor=value,") - assert request_headers["baggage"].count("sentry-trace_id=") == 1 - # added `baggage` and `sentry-trace` are not in the signed header set. - assert get_aws_sigv4_signed_headers( - authorization=request_headers.get("Authorization", "") - ) == { - "host", - "x-amz-date", - } - - -@pytest.mark.asyncio -@pytest.mark.parametrize("span_streaming", [False, True]) -async def test_outgoing_trace_headers_skip_query_signed_baggage( - sentry_init, aiohttp_raw_server, aiohttp_client, span_streaming -): - sentry_init( - integrations=[AioHttpIntegration()], - traces_sample_rate=1.0, - trace_lifecycle="stream" if span_streaming else "static", - default_integrations=False, - ) - - received_headers = [] - received_urls = [] - - async def handler(request): - received_headers.append(request.headers.copy()) - # Use the raw target so aiohttp/yarl URL canonicalization does not - # change the encoding of the presigned query parameters. - received_urls.append(request.raw_path) - return web.Response(text="OK") - - raw_server = await aiohttp_raw_server(handler) - path = ( - "/" - "?X-Amz-Algorithm=AWS4-HMAC-SHA256" - "&X-Amz-Credential=" - "test%2F20260804%2Feu-west-1%2Fs3%2Faws4_request" - "&X-Amz-Date=20260804T120000Z" - "&X-Amz-Expires=60" - "&X-Amz-SignedHeaders=baggage%3Bhost" - "&X-Amz-Signature=sixtyseven" - ) - - client = await aiohttp_client(raw_server) - - if span_streaming: - with sentry_sdk.traces.start_span(name="test"): # type: ignore[attr-defined] - response = await client.get( - path, - headers={"baggage": "vendor=value"}, - ) - else: - with sentry_sdk.start_transaction(name="test", sampled=True): - response = await client.get( - path, - headers={"baggage": "vendor=value"}, - ) - - headers = received_headers[0] - assert response.status == 200 - # `baggage` is part of X-Amz-SignedHeaders, so it must not be modified. - assert len(headers.getall("baggage")) == 1 - assert headers["baggage"] == "vendor=value" - # `sentry-trace` was not signed, so it can be propagated. - assert len(headers.getall("sentry-trace")) == 1 - assert get_aws_sigv4_signed_headers( - authorization=headers.get("Authorization", ""), url=received_urls[0] - ) == { - "baggage", - "host", - } - - @pytest.mark.asyncio async def test_request_source_disabled( sentry_init, From 6ff70c4dde7f02db07617e7cd7478e451b14ce4f Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 24 Aug 2026 10:00:41 +0200 Subject: [PATCH 29/37] limit changes to botocore connection classes --- sentry_sdk/integrations/stdlib.py | 198 +++++++++++++++++++----------- 1 file changed, 128 insertions(+), 70 deletions(-) diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index 2f513ecfa7..d6013b9fda 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -40,6 +40,11 @@ "build": sys.version, } +try: + from botocore.awsrequest import AWSHTTPConnection, AWSHTTPSConnection +except ImportError: + AWSHTTPConnection = None # type: ignore[misc,assignment] + AWSHTTPSConnection = None # type: ignore[misc,assignment] class StdlibIntegration(Integration): identifier = "stdlib" @@ -72,17 +77,12 @@ def _complete_span(span: "Union[Span, StreamedSpan]") -> None: with capture_internal_exceptions(): add_http_request_source(span) - -def _install_httplib() -> None: - real_putrequest = HTTPConnection.putrequest - real_putheader = HTTPConnection.putheader - real_endheaders = HTTPConnection.endheaders - real_getresponse = HTTPConnection.getresponse - real_read = HTTPResponse.read - real_close = HTTPResponse.close - +def _get_wrapped_putheader(original_putheader: "Callable[..., Any]") -> "Callable[..., Any]": + """ + Responsible for adding to `_sentrysdk_signed_headers` to keep track of signed headers. + """ def putheader(self: "HTTPConnection", header: "Any", *values: "Any") -> "Any": - rv = real_putheader(self, header, *values) + rv = original_putheader(self, header, *values) request_header_names = getattr(self, "_sentrysdk_request_headers", None) if request_header_names is None: @@ -107,6 +107,107 @@ def putheader(self: "HTTPConnection", header: "Any", *values: "Any") -> "Any": return rv + return putheader + +def _get_wrapped_endheaders(original_endheaders: "Callable[..., Any]") -> "Callable[..., Any]": + """ + Responsible for injecting trace propagation headers, ensuring that the request is not invalidated + by honoring signed headers. + """ + def endheaders(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": + real_url = getattr(self, "_sentrysdk_trace_url", None) + span = getattr(self, "_sentrysdk_span", None) + + if real_url is not None: + with capture_internal_exceptions(): + existing_headers: "Set[str]" = getattr( + self, "_sentrysdk_request_headers", set() + ) + signed_headers: "Set[str]" = getattr( + self, "_sentrysdk_signed_headers", set() + ) + signed_headers.update( + get_aws_sigv4_signed_headers(authorization=None, url=real_url) + ) + + for ( + header_name, + header_value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( + span=span + ): + normalized_header = header_name.lower() + # preserve signed headers and avoid duplicate `sentry-trace`. + if normalized_header in existing_headers and ( + normalized_header != BAGGAGE_HEADER_NAME + or normalized_header in signed_headers + ): + continue + + logger.debug( + "[Tracing] Adding `{key}` header {value} to outgoing request to {real_url}.".format( + key=header_name, value=header_value, real_url=real_url + ) + ) + self.putheader(header_name, header_value) + return original_endheaders(self, *args, **kwargs) + + return endheaders + + +def _get_wrapped_putrequest(original_putrequest: "Callable[..., Any]") -> "Callable[..., Any]": + """ + Responsible for initializing `_sentrysdk_signed_headers` on the instance. + """ + def putrequest( + self: "HTTPConnection", method: str, url: str, *args: "Any", **kwargs: "Any" + ) -> "Any": + # track existing headers separately from signed so thatexisting unsigned + # baggage header can be extended while signed baggage stays unchanged. + self._sentrysdk_request_headers = set() # type: ignore[attr-defined] + self._sentrysdk_signed_headers = set() # type: ignore[attr-defined] + + try: + rv = original_putrequest(self, method, url, *args, **kwargs) + except BaseException: + self._sentrysdk_request_headers = None # type: ignore[attr-defined] + self._sentrysdk_signed_headers = None # type: ignore[attr-defined] + raise + + return rv + + return putrequest + + +def _patch_aws_connection() -> None: + """ + Patch AWS connection classes. These classes provide functions to sign HTTP headers, and subsequently + injecting trace propagation headers would invalidate the request. + + Detect which signed headers by patching `putheader()`. Record headers on the `_sentrysdk_signed_headers` + set on the connection instance. The set is initialized in the `putrequest()` patch. + + Do not edit signed headers when adding trace propagation headers in the `endheaders()` patch. + """ + if AWSHTTPConnection is not None: + AWSHTTPConnection.putheader = _get_wrapped_putheader(AWSHTTPConnection.putheader) # type: ignore[method-assign] + AWSHTTPConnection.endheaders = _get_wrapped_endheaders(AWSHTTPConnection.endheaders) # type: ignore[method-assign] + AWSHTTPConnection.putrequest = _get_wrapped_putrequest(AWSHTTPConnection.putrequest) # type: ignore[method-assign] + + if AWSHTTPSConnection is not None: + AWSHTTPSConnection.putheader = _get_wrapped_putheader(AWSHTTPSConnection.putheader) # type: ignore[method-assign] + AWSHTTPSConnection.endheaders = _get_wrapped_endheaders(AWSHTTPSConnection.endheaders) # type: ignore[method-assign] + AWSHTTPSConnection.putrequest = _get_wrapped_putrequest(AWSHTTPSConnection.putrequest) # type: ignore[method-assign] + + +def _install_httplib() -> None: + _patch_aws_connection() + + real_putrequest = HTTPConnection.putrequest + real_getresponse = HTTPConnection.getresponse + real_read = HTTPResponse.read + real_close = HTTPResponse.close + def putrequest( self: "HTTPConnection", method: str, url: str, *args: "Any", **kwargs: "Any" ) -> "Any": @@ -186,71 +287,30 @@ def putrequest( set_on_span(SPANDATA.NETWORK_PEER_ADDRESS, self.host) set_on_span(SPANDATA.NETWORK_PEER_PORT, self.port) - # track existing headers separately from signed so thatexisting unsigned - # baggage header can be extended while signed baggage stays unchanged. - self._sentrysdk_request_headers = set() # type: ignore[attr-defined] - self._sentrysdk_signed_headers = set() # type: ignore[attr-defined] - - try: - rv = real_putrequest(self, method, url, *args, **kwargs) - except BaseException: - self._sentrysdk_trace_url = None # type: ignore[attr-defined] - self._sentrysdk_request_headers = None # type: ignore[attr-defined] - self._sentrysdk_signed_headers = None # type: ignore[attr-defined] - raise + rv = real_putrequest(self, method, url, *args, **kwargs) - if should_propagate_trace(client, real_url): + # If _sentrysdk_request_headers is present, trace propagation headers should + # be injected in an `endheaders()` patch. + if should_propagate_trace(client, real_url) and not hasattr(self, "_sentrysdk_request_headers"): + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( + span=span + ): + logger.debug( + "[Tracing] Adding `{key}` header {value} to outgoing request to {real_url}.".format( + key=key, value=value, real_url=real_url + ) + ) + self.putheader(key, value) + elif should_propagate_trace(client, real_url): self._sentrysdk_trace_url = real_url # type: ignore[attr-defined] - else: - self._sentrysdk_trace_url = None # type: ignore[attr-defined] self._sentrysdk_span = span # type: ignore[attr-defined] return rv - def endheaders(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": - real_url = getattr(self, "_sentrysdk_trace_url", None) - span = getattr(self, "_sentrysdk_span", None) - - try: - if real_url is not None: - with capture_internal_exceptions(): - existing_headers: "Set[str]" = getattr( - self, "_sentrysdk_request_headers", set() - ) - signed_headers: "Set[str]" = getattr( - self, "_sentrysdk_signed_headers", set() - ) - signed_headers.update( - get_aws_sigv4_signed_headers(authorization=None, url=real_url) - ) - - for ( - header_name, - header_value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( - span=span - ): - normalized_header = header_name.lower() - # preserve signed headers and avoid duplicate `sentry-trace`. - if normalized_header in existing_headers and ( - normalized_header != BAGGAGE_HEADER_NAME - or normalized_header in signed_headers - ): - continue - - logger.debug( - "[Tracing] Adding `{key}` header {value} to outgoing request to {real_url}.".format( - key=header_name, value=header_value, real_url=real_url - ) - ) - self.putheader(header_name, header_value) - return real_endheaders(self, *args, **kwargs) - finally: - self._sentrysdk_trace_url = None # type: ignore[attr-defined] - self._sentrysdk_request_headers = None # type: ignore[attr-defined] - self._sentrysdk_signed_headers = None # type: ignore[attr-defined] - def getresponse(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": span = getattr(self, "_sentrysdk_span", None) @@ -306,9 +366,7 @@ def close(self: "HTTPResponse") -> None: self._sentrysdk_span = None # type: ignore[attr-defined] _complete_span(span) - HTTPConnection.putheader = putheader # type: ignore[method-assign] HTTPConnection.putrequest = putrequest # type: ignore[method-assign] - HTTPConnection.endheaders = endheaders # type: ignore[method-assign] HTTPConnection.getresponse = getresponse # type: ignore[method-assign] HTTPResponse.read = read # type: ignore[method-assign] HTTPResponse.close = close # type: ignore[assignment,method-assign] From b68e9f755aebcdb77b42d91ba95250f1c9a752b1 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 24 Aug 2026 10:03:28 +0200 Subject: [PATCH 30/37] formatting --- sentry_sdk/integrations/stdlib.py | 46 ++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index 8e06b0d336..16ac4a22b1 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -47,6 +47,7 @@ AWSHTTPConnection = None # type: ignore[misc,assignment] AWSHTTPSConnection = None # type: ignore[misc,assignment] + class StdlibIntegration(Integration): identifier = "stdlib" @@ -78,10 +79,14 @@ def _complete_span(span: "Union[Span, StreamedSpan]") -> None: with capture_internal_exceptions(): add_http_request_source(span) -def _get_wrapped_putheader(original_putheader: "Callable[..., Any]") -> "Callable[..., Any]": + +def _get_wrapped_putheader( + original_putheader: "Callable[..., Any]", +) -> "Callable[..., Any]": """ Responsible for adding to `_sentrysdk_signed_headers` to keep track of signed headers. """ + def putheader(self: "HTTPConnection", header: "Any", *values: "Any") -> "Any": rv = original_putheader(self, header, *values) @@ -110,11 +115,15 @@ def putheader(self: "HTTPConnection", header: "Any", *values: "Any") -> "Any": return putheader -def _get_wrapped_endheaders(original_endheaders: "Callable[..., Any]") -> "Callable[..., Any]": + +def _get_wrapped_endheaders( + original_endheaders: "Callable[..., Any]", +) -> "Callable[..., Any]": """ Responsible for injecting trace propagation headers, ensuring that the request is not invalidated by honoring signed headers. """ + def endheaders(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": real_url = getattr(self, "_sentrysdk_trace_url", None) span = getattr(self, "_sentrysdk_span", None) @@ -156,10 +165,13 @@ def endheaders(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": return endheaders -def _get_wrapped_putrequest(original_putrequest: "Callable[..., Any]") -> "Callable[..., Any]": +def _get_wrapped_putrequest( + original_putrequest: "Callable[..., Any]", +) -> "Callable[..., Any]": """ Responsible for initializing `_sentrysdk_signed_headers` on the instance. """ + def putrequest( self: "HTTPConnection", method: str, url: str, *args: "Any", **kwargs: "Any" ) -> "Any": @@ -191,14 +203,26 @@ def _patch_aws_connection() -> None: Do not edit signed headers when adding trace propagation headers in the `endheaders()` patch. """ if AWSHTTPConnection is not None: - AWSHTTPConnection.putheader = _get_wrapped_putheader(AWSHTTPConnection.putheader) # type: ignore[method-assign] - AWSHTTPConnection.endheaders = _get_wrapped_endheaders(AWSHTTPConnection.endheaders) # type: ignore[method-assign] - AWSHTTPConnection.putrequest = _get_wrapped_putrequest(AWSHTTPConnection.putrequest) # type: ignore[method-assign] + AWSHTTPConnection.putheader = _get_wrapped_putheader( + AWSHTTPConnection.putheader + ) # type: ignore[method-assign] + AWSHTTPConnection.endheaders = _get_wrapped_endheaders( + AWSHTTPConnection.endheaders + ) # type: ignore[method-assign] + AWSHTTPConnection.putrequest = _get_wrapped_putrequest( + AWSHTTPConnection.putrequest + ) # type: ignore[method-assign] if AWSHTTPSConnection is not None: - AWSHTTPSConnection.putheader = _get_wrapped_putheader(AWSHTTPSConnection.putheader) # type: ignore[method-assign] - AWSHTTPSConnection.endheaders = _get_wrapped_endheaders(AWSHTTPSConnection.endheaders) # type: ignore[method-assign] - AWSHTTPSConnection.putrequest = _get_wrapped_putrequest(AWSHTTPSConnection.putrequest) # type: ignore[method-assign] + AWSHTTPSConnection.putheader = _get_wrapped_putheader( + AWSHTTPSConnection.putheader + ) # type: ignore[method-assign] + AWSHTTPSConnection.endheaders = _get_wrapped_endheaders( + AWSHTTPSConnection.endheaders + ) # type: ignore[method-assign] + AWSHTTPSConnection.putrequest = _get_wrapped_putrequest( + AWSHTTPSConnection.putrequest + ) # type: ignore[method-assign] def _install_httplib() -> None: @@ -321,7 +345,9 @@ def putrequest( # If _sentrysdk_request_headers is present, trace propagation headers should # be injected in an `endheaders()` patch. - if should_propagate_trace(client, real_url) and not hasattr(self, "_sentrysdk_request_headers"): + if should_propagate_trace(client, real_url) and not hasattr( + self, "_sentrysdk_request_headers" + ): for ( key, value, From 3ce7587fb210390b90d8c998207bd490334350fa Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 24 Aug 2026 10:05:18 +0200 Subject: [PATCH 31/37] make mypy happy --- sentry_sdk/integrations/stdlib.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index 16ac4a22b1..5dc61ed872 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -203,26 +203,26 @@ def _patch_aws_connection() -> None: Do not edit signed headers when adding trace propagation headers in the `endheaders()` patch. """ if AWSHTTPConnection is not None: - AWSHTTPConnection.putheader = _get_wrapped_putheader( + AWSHTTPConnection.putheader = _get_wrapped_putheader( # type: ignore[method-assign] AWSHTTPConnection.putheader - ) # type: ignore[method-assign] - AWSHTTPConnection.endheaders = _get_wrapped_endheaders( + ) + AWSHTTPConnection.endheaders = _get_wrapped_endheaders( # type: ignore[method-assign] AWSHTTPConnection.endheaders - ) # type: ignore[method-assign] - AWSHTTPConnection.putrequest = _get_wrapped_putrequest( + ) + AWSHTTPConnection.putrequest = _get_wrapped_putrequest( # type: ignore[method-assign] AWSHTTPConnection.putrequest - ) # type: ignore[method-assign] + ) if AWSHTTPSConnection is not None: - AWSHTTPSConnection.putheader = _get_wrapped_putheader( + AWSHTTPSConnection.putheader = _get_wrapped_putheader( # type: ignore[method-assign] AWSHTTPSConnection.putheader - ) # type: ignore[method-assign] - AWSHTTPSConnection.endheaders = _get_wrapped_endheaders( + ) + AWSHTTPSConnection.endheaders = _get_wrapped_endheaders( # type: ignore[method-assign] AWSHTTPSConnection.endheaders - ) # type: ignore[method-assign] - AWSHTTPSConnection.putrequest = _get_wrapped_putrequest( + ) + AWSHTTPSConnection.putrequest = _get_wrapped_putrequest( # type: ignore[method-assign] AWSHTTPSConnection.putrequest - ) # type: ignore[method-assign] + ) def _install_httplib() -> None: From 628543e81ac6a4026feb19a1c7641d59ed12f94a Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 24 Aug 2026 10:48:48 +0200 Subject: [PATCH 32/37] clean up trace_url on endheaders --- sentry_sdk/integrations/stdlib.py | 63 ++++++++++++++++--------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index 5dc61ed872..f48ad8011f 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -128,39 +128,42 @@ def endheaders(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": real_url = getattr(self, "_sentrysdk_trace_url", None) span = getattr(self, "_sentrysdk_span", None) - if real_url is not None: - with capture_internal_exceptions(): - existing_headers: "Set[str]" = getattr( - self, "_sentrysdk_request_headers", set() - ) - signed_headers: "Set[str]" = getattr( - self, "_sentrysdk_signed_headers", set() - ) - signed_headers.update( - get_aws_sigv4_signed_headers(authorization=None, url=real_url) - ) + try: + if real_url is not None: + with capture_internal_exceptions(): + existing_headers: "Set[str]" = getattr( + self, "_sentrysdk_request_headers", set() + ) + signed_headers: "Set[str]" = getattr( + self, "_sentrysdk_signed_headers", set() + ) + signed_headers.update( + get_aws_sigv4_signed_headers(authorization=None, url=real_url) + ) - for ( - header_name, - header_value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( - span=span - ): - normalized_header = header_name.lower() - # preserve signed headers and avoid duplicate `sentry-trace`. - if normalized_header in existing_headers and ( - normalized_header != BAGGAGE_HEADER_NAME - or normalized_header in signed_headers + for ( + header_name, + header_value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( + span=span ): - continue - - logger.debug( - "[Tracing] Adding `{key}` header {value} to outgoing request to {real_url}.".format( - key=header_name, value=header_value, real_url=real_url + normalized_header = header_name.lower() + # preserve signed headers and avoid duplicate `sentry-trace`. + if normalized_header in existing_headers and ( + normalized_header != BAGGAGE_HEADER_NAME + or normalized_header in signed_headers + ): + continue + + logger.debug( + "[Tracing] Adding `{key}` header {value} to outgoing request to {real_url}.".format( + key=header_name, value=header_value, real_url=real_url + ) ) - ) - self.putheader(header_name, header_value) - return original_endheaders(self, *args, **kwargs) + self.putheader(header_name, header_value) + return original_endheaders(self, *args, **kwargs) + finally: + self._sentrysdk_trace_url = None # type: ignore[attr-defined] return endheaders From a739e844fec9fa6ac0f3fe03f3484e3fc697cb5c Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Mon, 24 Aug 2026 11:14:16 +0200 Subject: [PATCH 33/37] (ref): Make `get_aws_sigv4_signed_headers` private; renamed to `_get_aws_sigv4_signed_headers` --- sentry_sdk/integrations/stdlib.py | 6 ++-- sentry_sdk/utils.py | 2 +- .../boto3/test_trace_propagation.py | 8 ++--- tests/integrations/stdlib/test_httplib.py | 6 ++-- tests/test_utils.py | 30 +------------------ 5 files changed, 12 insertions(+), 40 deletions(-) diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index f48ad8011f..f35f9d7544 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -22,7 +22,7 @@ SENSITIVE_DATA_SUBSTITUTE, capture_internal_exceptions, ensure_integration_enabled, - get_aws_sigv4_signed_headers, + _get_aws_sigv4_signed_headers, is_sentry_url, logger, parse_url, @@ -109,7 +109,7 @@ def putheader(self: "HTTPConnection", header: "Any", *values: "Any") -> "Any": authorization = values[0] if isinstance(authorization, bytes): authorization = authorization.decode("ascii", "ignore") - signed_headers.update(get_aws_sigv4_signed_headers(authorization)) + signed_headers.update(_get_aws_sigv4_signed_headers(authorization)) return rv @@ -138,7 +138,7 @@ def endheaders(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": self, "_sentrysdk_signed_headers", set() ) signed_headers.update( - get_aws_sigv4_signed_headers(authorization=None, url=real_url) + _get_aws_sigv4_signed_headers(authorization=None, url=real_url) ) for ( diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index d79b04690f..934d370d93 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1697,7 +1697,7 @@ def parse_url(url: str, sanitize: bool = True) -> "ParsedUrl": ) -def get_aws_sigv4_signed_headers( +def _get_aws_sigv4_signed_headers( authorization: "Optional[str]", url: "Optional[str]" = None ) -> "Set[str]": diff --git a/tests/integrations/boto3/test_trace_propagation.py b/tests/integrations/boto3/test_trace_propagation.py index 147b8d23ca..82ba269f6c 100644 --- a/tests/integrations/boto3/test_trace_propagation.py +++ b/tests/integrations/boto3/test_trace_propagation.py @@ -9,7 +9,7 @@ import sentry_sdk from sentry_sdk.integrations.boto3 import Boto3Integration from sentry_sdk.integrations.stdlib import StdlibIntegration -from sentry_sdk.utils import get_aws_sigv4_signed_headers +from sentry_sdk.utils import _get_aws_sigv4_signed_headers class _AwsRequestHandler(BaseHTTPRequestHandler): @@ -114,7 +114,7 @@ def capture_headers_after_instrumentation(request, **kwargs): assert len(sentry_trace_headers) == 1 assert sentry_trace_headers == signed_request_headers["sentry-trace"] # both `baggage` and `sentry-trace` are signed. - signed_headers = get_aws_sigv4_signed_headers( + signed_headers = _get_aws_sigv4_signed_headers( authorization=headers.get("Authorization", "") ) assert signed_headers >= {"baggage", "sentry-trace"} @@ -172,7 +172,7 @@ def _inject_signed_baggage(request, **kwargs): assert headers.get_all("baggage") == ["vendor=value"] # `httplib` still adds single `sentry-trace` header. assert len(headers.get_all("sentry-trace")) == 1 - signed_headers = get_aws_sigv4_signed_headers( + signed_headers = _get_aws_sigv4_signed_headers( authorization=headers.get("Authorization", "") ) assert "baggage" in signed_headers @@ -205,7 +205,7 @@ def test_presigned_urls_do_not_require_sentry_headers(sentry_init): # only `host` header is signed. assert query["X-Amz-SignedHeaders"] == ["host"] - assert get_aws_sigv4_signed_headers(authorization=None, url=url) == {"host"} + assert _get_aws_sigv4_signed_headers(authorization=None, url=url) == {"host"} # no `sentry-*` or baggage are added. assert "sentry-trace" not in url assert "baggage" not in url diff --git a/tests/integrations/stdlib/test_httplib.py b/tests/integrations/stdlib/test_httplib.py index 907b1e7267..8a900f361a 100644 --- a/tests/integrations/stdlib/test_httplib.py +++ b/tests/integrations/stdlib/test_httplib.py @@ -16,7 +16,7 @@ from sentry_sdk import capture_message, continue_trace, start_transaction from sentry_sdk.consts import MATCH_ALL, SPANDATA from sentry_sdk.integrations.stdlib import StdlibIntegration -from sentry_sdk.utils import get_aws_sigv4_signed_headers +from sentry_sdk.utils import _get_aws_sigv4_signed_headers from tests.conftest import ApproxDict, create_mock_http_server, get_free_port PORT = create_mock_http_server() @@ -788,7 +788,7 @@ def test_outgoing_trace_headers_skip_signed_baggage( assert headers.get_all("baggage") == ["vendor=value"] # preserves existing `sentry-trace` header. assert headers.get_all("sentry-trace") == ["existing-trace"] - assert get_aws_sigv4_signed_headers( + assert _get_aws_sigv4_signed_headers( authorization=headers.get("Authorization", "") ) >= { "baggage", @@ -840,7 +840,7 @@ def test_outgoing_trace_headers_skip_query_signed_baggage( assert headers["baggage"] == "vendor=value" # `sentry-trace` was not signed, so it can be propagated. assert len(headers.get_all("sentry-trace")) == 1 - assert get_aws_sigv4_signed_headers( + assert _get_aws_sigv4_signed_headers( authorization=headers.get("Authorization", ""), url=f"http://127.0.0.1:{server.server_port}{path}", ) >= {"baggage", "host"} diff --git a/tests/test_utils.py b/tests/test_utils.py index c28f4f322c..12716332d5 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -18,7 +18,7 @@ env_to_bool, exc_info_from_error, format_timestamp, - get_aws_sigv4_signed_headers, + _get_aws_sigv4_signed_headers, get_current_thread_meta, get_default_release, get_error_message, @@ -665,34 +665,6 @@ def test_default_release_empty_string(): assert release is None - -@pytest.mark.parametrize( - "authorization,url,expected", - [ - ( - "AWS4-HMAC-SHA256 " - "Credential=test/20260804/eu-west-1/secretsmanager/aws4_request, " - "SignedHeaders=Host;X-Amz-Date, " - "Signature=sixtyseven", - None, - {"host", "x-amz-date"}, - ), - ( - "", - "https://example.com/?" - "X-Amz-Algorithm=AWS4-HMAC-SHA256&" - "X-Amz-SignedHeaders=host%3Bx-amz-date&" - "X-Amz-Signature=sixtyseven", - {"host", "x-amz-date"}, - ), - ], -) -def test_get_aws_sigv4_signed_headers(authorization, url, expected): - assert ( - get_aws_sigv4_signed_headers(authorization=authorization, url=url) == expected - ) - - def test_get_default_release_sentry_release_env(monkeypatch): monkeypatch.setenv("SENTRY_RELEASE", "sentry-env-release") assert get_default_release() == "sentry-env-release" From 3d44029eeb1ad52b65d14489bea872cf0bebe744 Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Mon, 24 Aug 2026 11:51:41 +0200 Subject: [PATCH 34/37] (tests): Move tests using `AWSHTTPConnection` to `test_aws_http_connection` --- .../boto3/test_aws_http_connection.py | 186 ++++++++++++++++++ tests/integrations/stdlib/test_httplib.py | 175 ---------------- 2 files changed, 186 insertions(+), 175 deletions(-) create mode 100644 tests/integrations/boto3/test_aws_http_connection.py diff --git a/tests/integrations/boto3/test_aws_http_connection.py b/tests/integrations/boto3/test_aws_http_connection.py new file mode 100644 index 0000000000..2c61a7b53d --- /dev/null +++ b/tests/integrations/boto3/test_aws_http_connection.py @@ -0,0 +1,186 @@ +from http.client import HTTPMessage +from http.server import BaseHTTPRequestHandler, HTTPServer +from threading import Thread +from unittest import mock + +import pytest +from botocore.awsrequest import AWSHTTPConnection + +import sentry_sdk +from sentry_sdk.integrations.stdlib import StdlibIntegration +from sentry_sdk.utils import _get_aws_sigv4_signed_headers + + +@pytest.fixture +def local_http_server(): + requests = [] + + class TraceHeaderHandler(BaseHTTPRequestHandler): + def do_POST(self): + requests.append(self.headers) + self.send_response(200) + self.send_header("Content-Length", "0") + self.end_headers() + + server = HTTPServer(("127.0.0.1", 0), TraceHeaderHandler) + thread = Thread(target=server.serve_forever, daemon=True) + thread.start() + + try: + yield server, requests + finally: + server.shutdown() + server.server_close() + thread.join() + + +def _request(server, headers, path="/"): + connection = AWSHTTPConnection("127.0.0.1", server.server_port) + connection.request("POST", path, headers=dict(headers)) + + response = connection.getresponse() + response.read() + connection.close() + + +@pytest.mark.parametrize("span_streaming", [False, True]) +def test_aws_http_connection_appends_to_unsigned_baggage( + sentry_init, local_http_server, span_streaming +): + sentry_init( + traces_sample_rate=1.0, + trace_lifecycle="stream" if span_streaming else "static", + default_integrations=False, + integrations=[StdlibIntegration()], + ) + server, requests = local_http_server + + with mock.patch("sentry_sdk.tracing_utils.Random.randrange", return_value=67): + if span_streaming: + with sentry_sdk.traces.start_span(name="test"): # type: ignore[attr-defined] + _request(server, [("baggage", "vendor=value")]) + else: + with sentry_sdk.start_transaction(name="test", sampled=True): + _request(server, [("baggage", "vendor=value")]) + + headers: HTTPMessage = requests[0] + + # preserve existing unsigned baggage + baggage_headers = headers.get_all("baggage") + assert baggage_headers is not None + assert len(baggage_headers) == 2 + assert baggage_headers[0] == "vendor=value" + assert baggage_headers[1].count("sentry-trace_id=") == 1 + assert "sentry-sample_rand=0.000067" in baggage_headers[1] + sentry_trace_headers = headers.get_all("sentry-trace") + assert sentry_trace_headers is not None + assert len(sentry_trace_headers) == 1 + + +@pytest.mark.parametrize("span_streaming", [False, True]) +def test_aws_http_connection_skips_signed_baggage( + sentry_init, local_http_server, span_streaming +): + sentry_init( + traces_sample_rate=1.0, + trace_lifecycle="stream" if span_streaming else "static", + default_integrations=False, + integrations=[StdlibIntegration()], + ) + server, requests = local_http_server + + # simulate AWS SigV4 request that is already signed. + authorization = ( + "AWS4-HMAC-SHA256 " + "Credential=test/20260804/eu-west-1/secretsmanager/aws4_request, " + "SignedHeaders=baggage;host;sentry-trace, " + "Signature=sixtyseven" + ) + + if span_streaming: + with sentry_sdk.traces.start_span(name="test"): # type: ignore[attr-defined] + _request( + server, + [ + ("baggage", "vendor=value"), + ("sentry-trace", "existing-trace"), + ("Authorization", authorization), + ], + ) + else: + with sentry_sdk.start_transaction(name="test", sampled=True): + _request( + server, + [ + ("baggage", "vendor=value"), + ("sentry-trace", "existing-trace"), + ("Authorization", authorization), + ], + ) + + headers: HTTPMessage = requests[0] + + # do not append baggage after SigV4 signs it. + assert headers.get_all("baggage") == ["vendor=value"] + # preserves existing `sentry-trace` header. + assert headers.get_all("sentry-trace") == ["existing-trace"] + assert _get_aws_sigv4_signed_headers( + authorization=headers.get("Authorization", "") + ) >= { + "baggage", + "host", + "sentry-trace", + } + + +@pytest.mark.parametrize("span_streaming", [False, True]) +def test_aws_http_connection_skips_query_signed_baggage( + sentry_init, local_http_server, span_streaming +): + sentry_init( + traces_sample_rate=1.0, + trace_lifecycle="stream" if span_streaming else "static", + default_integrations=False, + integrations=[StdlibIntegration()], + ) + server, requests = local_http_server + path = ( + "/" + "?X-Amz-Algorithm=AWS4-HMAC-SHA256" + "&X-Amz-Credential=" + "test%2F20260804%2Feu-west-1%2Fs3%2Faws4_request" + "&X-Amz-Date=20260804T120000Z" + "&X-Amz-Expires=60" + "&X-Amz-SignedHeaders=baggage%3Bhost" + "&X-Amz-Signature=sixtyseven" + ) + + if span_streaming: + with sentry_sdk.traces.start_span(name="test"): # type: ignore[attr-defined] + _request( + server, + [("baggage", "vendor=value")], + path=path, + ) + else: + with sentry_sdk.start_transaction(name="test", sampled=True): + _request( + server, + [("baggage", "vendor=value")], + path=path, + ) + + headers: HTTPMessage = requests[0] + # `baggage` is part of X-Amz-SignedHeaders, so may not be modified. + baggage_headers = headers.get_all("baggage") + assert baggage_headers is not None + assert len(baggage_headers) == 1 + assert headers["baggage"] == "vendor=value" + # `sentry-trace` was not signed, so it can be propagated. + sentry_trace_headers = headers.get_all("sentry-trace") + assert sentry_trace_headers is not None + assert len(sentry_trace_headers) == 1 + assert _get_aws_sigv4_signed_headers( + authorization=headers.get("Authorization", ""), + url=f"http://127.0.0.1:{server.server_port}{path}", + ) >= {"baggage", "host"} diff --git a/tests/integrations/stdlib/test_httplib.py b/tests/integrations/stdlib/test_httplib.py index 8a900f361a..60485595f3 100644 --- a/tests/integrations/stdlib/test_httplib.py +++ b/tests/integrations/stdlib/test_httplib.py @@ -16,7 +16,6 @@ from sentry_sdk import capture_message, continue_trace, start_transaction from sentry_sdk.consts import MATCH_ALL, SPANDATA from sentry_sdk.integrations.stdlib import StdlibIntegration -from sentry_sdk.utils import _get_aws_sigv4_signed_headers from tests.conftest import ApproxDict, create_mock_http_server, get_free_port PORT = create_mock_http_server() @@ -77,43 +76,6 @@ def create_chunked_server(): CHUNKED_PORT = create_chunked_server() -@pytest.fixture -def local_http_server(): - requests = [] - - class TraceHeaderHandler(BaseHTTPRequestHandler): - def do_POST(self): - requests.append(self.headers) - self.send_response(200) - self.send_header("Content-Length", "0") - self.end_headers() - - server = HTTPServer(("127.0.0.1", 0), TraceHeaderHandler) - thread = Thread(target=server.serve_forever, daemon=True) - thread.start() - - try: - yield server, requests - finally: - server.shutdown() - server.server_close() - thread.join() - - -def _request(server, headers, path="/"): - connection = HTTPConnection("127.0.0.1", server.server_port) - connection.putrequest("POST", path) - - for key, value in headers: - connection.putheader(key, value) - - connection.endheaders() - - response = connection.getresponse() - response.read() - connection.close() - - def test_crumb_capture(sentry_init, capture_events): sentry_init(integrations=[StdlibIntegration()], send_default_pii=True) events = capture_events() @@ -709,143 +671,6 @@ def getresponse(self, *args, **kwargs): assert request_headers["baggage"] == expected_outgoing_baggage -@pytest.mark.parametrize("span_streaming", [False, True]) -def test_outgoing_trace_headers_append_to_unsigned_baggage( - sentry_init, local_http_server, span_streaming -): - sentry_init( - traces_sample_rate=1.0, - trace_lifecycle="stream" if span_streaming else "static", - default_integrations=False, - integrations=[StdlibIntegration()], - ) - server, requests = local_http_server - - with mock.patch("sentry_sdk.tracing_utils.Random.randrange", return_value=67): - if span_streaming: - with sentry_sdk.traces.start_span(name="test"): # type: ignore[attr-defined] - _request(server, [("baggage", "vendor=value")]) - else: - with sentry_sdk.start_transaction(name="test", sampled=True): - _request(server, [("baggage", "vendor=value")]) - - headers = requests[0] - - # preserve existing unsigned baggage - baggage_headers = headers.get_all("baggage") - assert baggage_headers is not None - assert len(baggage_headers) == 2 - assert baggage_headers[0] == "vendor=value" - assert baggage_headers[1].count("sentry-trace_id=") == 1 - assert "sentry-sample_rand=0.000067" in baggage_headers[1] - assert len(headers.get_all("sentry-trace")) == 1 - - -@pytest.mark.parametrize("span_streaming", [False, True]) -def test_outgoing_trace_headers_skip_signed_baggage( - sentry_init, local_http_server, span_streaming -): - sentry_init( - traces_sample_rate=1.0, - trace_lifecycle="stream" if span_streaming else "static", - default_integrations=False, - integrations=[StdlibIntegration()], - ) - server, requests = local_http_server - - # simulate AWS SigV4 request that is already signed. - authorization = ( - "AWS4-HMAC-SHA256 " - "Credential=test/20260804/eu-west-1/secretsmanager/aws4_request, " - "SignedHeaders=baggage;host;sentry-trace, " - "Signature=sixtyseven" - ) - - if span_streaming: - with sentry_sdk.traces.start_span(name="test"): # type: ignore[attr-defined] - _request( - server, - [ - ("baggage", "vendor=value"), - ("sentry-trace", "existing-trace"), - ("Authorization", authorization), - ], - ) - else: - with sentry_sdk.start_transaction(name="test", sampled=True): - _request( - server, - [ - ("baggage", "vendor=value"), - ("sentry-trace", "existing-trace"), - ("Authorization", authorization), - ], - ) - - headers = requests[0] - - # do not append baggage after SigV4 signs it. - assert headers.get_all("baggage") == ["vendor=value"] - # preserves existing `sentry-trace` header. - assert headers.get_all("sentry-trace") == ["existing-trace"] - assert _get_aws_sigv4_signed_headers( - authorization=headers.get("Authorization", "") - ) >= { - "baggage", - "host", - "sentry-trace", - } - - -@pytest.mark.parametrize("span_streaming", [False, True]) -def test_outgoing_trace_headers_skip_query_signed_baggage( - sentry_init, local_http_server, span_streaming -): - sentry_init( - traces_sample_rate=1.0, - trace_lifecycle="stream" if span_streaming else "static", - default_integrations=False, - integrations=[StdlibIntegration()], - ) - server, requests = local_http_server - path = ( - "/" - "?X-Amz-Algorithm=AWS4-HMAC-SHA256" - "&X-Amz-Credential=" - "test%2F20260804%2Feu-west-1%2Fs3%2Faws4_request" - "&X-Amz-Date=20260804T120000Z" - "&X-Amz-Expires=60" - "&X-Amz-SignedHeaders=baggage%3Bhost" - "&X-Amz-Signature=sixtyseven" - ) - - if span_streaming: - with sentry_sdk.traces.start_span(name="test"): # type: ignore[attr-defined] - _request( - server, - [("baggage", "vendor=value")], - path=path, - ) - else: - with sentry_sdk.start_transaction(name="test", sampled=True): - _request( - server, - [("baggage", "vendor=value")], - path=path, - ) - - headers = requests[0] - # `baggage` is part of X-Amz-SignedHeaders, so may not be modified. - assert len(headers.get_all("baggage")) == 1 - assert headers["baggage"] == "vendor=value" - # `sentry-trace` was not signed, so it can be propagated. - assert len(headers.get_all("sentry-trace")) == 1 - assert _get_aws_sigv4_signed_headers( - authorization=headers.get("Authorization", ""), - url=f"http://127.0.0.1:{server.server_port}{path}", - ) >= {"baggage", "host"} - - @pytest.mark.parametrize( "trace_propagation_targets,host,path,trace_propagated", [ From c706b7bef1156728fbc45834f9bbaf9b52036560 Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Mon, 24 Aug 2026 13:34:44 +0200 Subject: [PATCH 35/37] remove tracking of uncessary headers --- sentry_sdk/integrations/stdlib.py | 112 +++++++++++++++++------------- tests/test_utils.py | 1 + 2 files changed, 65 insertions(+), 48 deletions(-) diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index f35f9d7544..7bc0c86ad6 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -20,9 +20,9 @@ ) from sentry_sdk.utils import ( SENSITIVE_DATA_SUBSTITUTE, + _get_aws_sigv4_signed_headers, capture_internal_exceptions, ensure_integration_enabled, - _get_aws_sigv4_signed_headers, is_sentry_url, logger, parse_url, @@ -30,7 +30,7 @@ ) if TYPE_CHECKING: - from typing import Any, Callable, Dict, List, Optional, Set, Union + from typing import Any, Callable, Dict, List, Optional, Tuple, Union from sentry_sdk._types import Event, Hint @@ -41,6 +41,8 @@ "build": sys.version, } +_SENTRY_HEADER_NAMES = frozenset((BAGGAGE_HEADER_NAME, "sentry-trace")) + try: from botocore.awsrequest import AWSHTTPConnection, AWSHTTPSConnection except ImportError: @@ -84,14 +86,16 @@ def _get_wrapped_putheader( original_putheader: "Callable[..., Any]", ) -> "Callable[..., Any]": """ - Responsible for adding to `_sentrysdk_signed_headers` to keep track of signed headers. + Responsible for tracking request and signed headers. """ def putheader(self: "HTTPConnection", header: "Any", *values: "Any") -> "Any": rv = original_putheader(self, header, *values) - request_header_names = getattr(self, "_sentrysdk_request_headers", None) - if request_header_names is None: + request_headers: "Optional[Dict[str, Tuple[bool, bool]]]" = getattr( + self, "_sentrysdk_request_headers", None + ) + if request_headers is None: return rv if isinstance(header, bytes): @@ -101,15 +105,21 @@ def putheader(self: "HTTPConnection", header: "Any", *values: "Any") -> "Any": else: return rv - request_header_names.add(normalized_header) + if normalized_header in _SENTRY_HEADER_NAMES: + _, is_signed = request_headers.get(normalized_header, (False, False)) + request_headers[normalized_header] = (True, is_signed) + if normalized_header == "authorization" and values: - signed_headers = getattr(self, "_sentrysdk_signed_headers", None) - if signed_headers is not None: - with capture_internal_exceptions(): - authorization = values[0] - if isinstance(authorization, bytes): - authorization = authorization.decode("ascii", "ignore") - signed_headers.update(_get_aws_sigv4_signed_headers(authorization)) + with capture_internal_exceptions(): + authorization = values[0] + if isinstance(authorization, bytes): + authorization = authorization.decode("ascii", "ignore") + for signed_header in _get_aws_sigv4_signed_headers(authorization): + if signed_header in _SENTRY_HEADER_NAMES: + is_present, _ = request_headers.get( + signed_header, (False, False) + ) + request_headers[signed_header] = (is_present, True) return rv @@ -131,36 +141,43 @@ def endheaders(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": try: if real_url is not None: with capture_internal_exceptions(): - existing_headers: "Set[str]" = getattr( - self, "_sentrysdk_request_headers", set() + request_headers: "Optional[Dict[str, Tuple[bool, bool]]]" = getattr( + self, "_sentrysdk_request_headers", {} ) - signed_headers: "Set[str]" = getattr( - self, "_sentrysdk_signed_headers", set() - ) - signed_headers.update( - _get_aws_sigv4_signed_headers(authorization=None, url=real_url) - ) - - for ( - header_name, - header_value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( - span=span - ): - normalized_header = header_name.lower() - # preserve signed headers and avoid duplicate `sentry-trace`. - if normalized_header in existing_headers and ( - normalized_header != BAGGAGE_HEADER_NAME - or normalized_header in signed_headers + if request_headers is not None: + for signed_header in _get_aws_sigv4_signed_headers( + authorization=None, url=real_url ): - continue - - logger.debug( - "[Tracing] Adding `{key}` header {value} to outgoing request to {real_url}.".format( - key=header_name, value=header_value, real_url=real_url + if signed_header in _SENTRY_HEADER_NAMES: + is_present, _ = request_headers.get( + signed_header, (False, False) + ) + request_headers[signed_header] = (is_present, True) + + for ( + header_name, + header_value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( + span=span + ): + normalized_header = header_name.lower() + # preserve signed headers and avoid duplicate `sentry-trace`. + is_present, is_signed = request_headers.get( + normalized_header, (False, False) ) - ) - self.putheader(header_name, header_value) + if is_present and ( + normalized_header != BAGGAGE_HEADER_NAME or is_signed + ): + continue + + logger.debug( + "[Tracing] Adding `{key}` header {value} to outgoing request to {real_url}.".format( + key=header_name, + value=header_value, + real_url=real_url, + ) + ) + self.putheader(header_name, header_value) return original_endheaders(self, *args, **kwargs) finally: self._sentrysdk_trace_url = None # type: ignore[attr-defined] @@ -172,22 +189,20 @@ def _get_wrapped_putrequest( original_putrequest: "Callable[..., Any]", ) -> "Callable[..., Any]": """ - Responsible for initializing `_sentrysdk_signed_headers` on the instance. + Responsible for initializing request and signed header tracking on the instance. """ def putrequest( self: "HTTPConnection", method: str, url: str, *args: "Any", **kwargs: "Any" ) -> "Any": - # track existing headers separately from signed so thatexisting unsigned - # baggage header can be extended while signed baggage stays unchanged. - self._sentrysdk_request_headers = set() # type: ignore[attr-defined] - self._sentrysdk_signed_headers = set() # type: ignore[attr-defined] + # track which propagation headers are present and signed e.g. {"sentry-trace": (is_present, is_signed)} + request_headers: "Optional[Dict[str, Tuple[bool, bool]]]" = {} + self._sentrysdk_request_headers = request_headers # type: ignore[attr-defined] try: rv = original_putrequest(self, method, url, *args, **kwargs) except BaseException: self._sentrysdk_request_headers = None # type: ignore[attr-defined] - self._sentrysdk_signed_headers = None # type: ignore[attr-defined] raise return rv @@ -200,8 +215,9 @@ def _patch_aws_connection() -> None: Patch AWS connection classes. These classes provide functions to sign HTTP headers, and subsequently injecting trace propagation headers would invalidate the request. - Detect which signed headers by patching `putheader()`. Record headers on the `_sentrysdk_signed_headers` - set on the connection instance. The set is initialized in the `putrequest()` patch. + Detect whether propagation headers are present and signed by patching + `putheader()`. Store that state in `_sentrysdk_request_headers`, initialized + by the `putrequest()` patch. Do not edit signed headers when adding trace propagation headers in the `endheaders()` patch. """ diff --git a/tests/test_utils.py b/tests/test_utils.py index 12716332d5..4bc40f9476 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -665,6 +665,7 @@ def test_default_release_empty_string(): assert release is None + def test_get_default_release_sentry_release_env(monkeypatch): monkeypatch.setenv("SENTRY_RELEASE", "sentry-env-release") assert get_default_release() == "sentry-env-release" From 1ee3b5839f29a0b84af9b30a286f8472aed9753f Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Mon, 24 Aug 2026 13:39:40 +0200 Subject: [PATCH 36/37] ruff fix --- tests/test_utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 4bc40f9476..64973ea5dd 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -18,7 +18,6 @@ env_to_bool, exc_info_from_error, format_timestamp, - _get_aws_sigv4_signed_headers, get_current_thread_meta, get_default_release, get_error_message, From 8762b88ab1faa034cecd0a61ccbd34c1542890af Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Tue, 25 Aug 2026 10:44:50 +0200 Subject: [PATCH 37/37] resolve PR comments --- sentry_sdk/integrations/boto3.py | 8 +++- sentry_sdk/integrations/stdlib.py | 28 +++++++---- sentry_sdk/utils.py | 46 +++++++++---------- .../boto3/test_aws_http_connection.py | 14 +++--- .../boto3/test_trace_propagation.py | 15 +++--- 5 files changed, 65 insertions(+), 46 deletions(-) diff --git a/sentry_sdk/integrations/boto3.py b/sentry_sdk/integrations/boto3.py index 6848253cfe..65b28b9d96 100644 --- a/sentry_sdk/integrations/boto3.py +++ b/sentry_sdk/integrations/boto3.py @@ -169,7 +169,13 @@ def _sentry_before_sign( return def _replace_header(request: "AWSRequest", key: str, value: str) -> None: - # HTTPHeaders appends on assignment, so delete existing values first. + """ + Botocore's `HTTPHeaders` inherits from `email.message.Message`, where: + headers["foo"] = "old" + headers["foo"] = "new" + produces two fields: {"foo": "old", "foo": "new"}. So delete existing + fields before assigning replacement. + """ if key in request.headers: del request.headers[key] request.headers[key] = value diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index 7bc0c86ad6..534e940504 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -10,7 +10,7 @@ from sentry_sdk.integrations import Integration from sentry_sdk.scope import add_global_event_processor, should_send_default_pii from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import BAGGAGE_HEADER_NAME, Span +from sentry_sdk.tracing import BAGGAGE_HEADER_NAME, SENTRY_TRACE_HEADER_NAME, Span from sentry_sdk.tracing_utils import ( EnvironHeaders, add_http_breadcrumb, @@ -20,7 +20,8 @@ ) from sentry_sdk.utils import ( SENSITIVE_DATA_SUBSTITUTE, - _get_aws_sigv4_signed_headers, + _get_aws_sigv4_signed_headers_from_authorization_header, + _get_aws_sigv4_signed_headers_from_url_query_string, capture_internal_exceptions, ensure_integration_enabled, is_sentry_url, @@ -41,7 +42,7 @@ "build": sys.version, } -_SENTRY_HEADER_NAMES = frozenset((BAGGAGE_HEADER_NAME, "sentry-trace")) +_SENTRY_HEADER_NAMES = frozenset((BAGGAGE_HEADER_NAME, SENTRY_TRACE_HEADER_NAME)) try: from botocore.awsrequest import AWSHTTPConnection, AWSHTTPSConnection @@ -86,7 +87,8 @@ def _get_wrapped_putheader( original_putheader: "Callable[..., Any]", ) -> "Callable[..., Any]": """ - Responsible for tracking request and signed headers. + Responsible for tracking which sentry headers are present and whether + they are listed in AWS SigV4 `SignedHeaders`. """ def putheader(self: "HTTPConnection", header: "Any", *values: "Any") -> "Any": @@ -113,8 +115,12 @@ def putheader(self: "HTTPConnection", header: "Any", *values: "Any") -> "Any": with capture_internal_exceptions(): authorization = values[0] if isinstance(authorization, bytes): - authorization = authorization.decode("ascii", "ignore") - for signed_header in _get_aws_sigv4_signed_headers(authorization): + authorization = authorization.decode("latin-1") + for ( + signed_header + ) in _get_aws_sigv4_signed_headers_from_authorization_header( + authorization + ): if signed_header in _SENTRY_HEADER_NAMES: is_present, _ = request_headers.get( signed_header, (False, False) @@ -145,8 +151,10 @@ def endheaders(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": self, "_sentrysdk_request_headers", {} ) if request_headers is not None: - for signed_header in _get_aws_sigv4_signed_headers( - authorization=None, url=real_url + for ( + signed_header + ) in _get_aws_sigv4_signed_headers_from_url_query_string( + real_url ): if signed_header in _SENTRY_HEADER_NAMES: is_present, _ = request_headers.get( @@ -165,8 +173,8 @@ def endheaders(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": is_present, is_signed = request_headers.get( normalized_header, (False, False) ) - if is_present and ( - normalized_header != BAGGAGE_HEADER_NAME or is_signed + if is_signed or ( + is_present and normalized_header != BAGGAGE_HEADER_NAME ): continue diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 934d370d93..9ba485b85f 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -87,6 +87,9 @@ FALSY_ENV_VALUES = frozenset(("false", "f", "n", "no", "off", "0")) TRUTHY_ENV_VALUES = frozenset(("true", "t", "y", "yes", "on", "1")) +_AWS_SIGV4_SIGNING_ALGORITHMS = frozenset( + ("AWS4-HMAC-SHA256", "AWS4-ECDSA-P256-SHA256") +) MAX_STACK_FRAMES = 2000 """Maximum number of stack frames to send to Sentry. @@ -1697,41 +1700,38 @@ def parse_url(url: str, sanitize: bool = True) -> "ParsedUrl": ) -def _get_aws_sigv4_signed_headers( - authorization: "Optional[str]", url: "Optional[str]" = None +def _get_aws_sigv4_signed_headers_from_authorization_header( + authorization: str, ) -> "Set[str]": + # only AWS SigV4 authorization has the SignedHeaders parameter. + value = authorization.lstrip() + algorithm, _, parameters = value.partition(" ") + if algorithm not in _AWS_SIGV4_SIGNING_ALGORITHMS: + return set() + + for part in parameters.split(","): + part = part.strip() + if part.startswith("SignedHeaders="): + _, _, header_names = part.partition("=") + return {header.lower() for header in header_names.split(";") if header} + + return set() - signed_headers: "Set[str]" = set() - if authorization is not None: - # only AWS SigV4 authorization has the SignedHeaders parameter. - value = authorization.lstrip() - if value.startswith(("AWS4-HMAC-SHA256", "AWS4-ECDSA-P256-SHA256")): - for part in value.split(","): - part = part.strip() - if part.startswith("SignedHeaders="): - _, _, header_names = part.partition("=") - signed_headers.update( - header.lower() for header in header_names.split(";") if header - ) - break - - if url is None: - return signed_headers +def _get_aws_sigv4_signed_headers_from_url_query_string(url: str) -> "Set[str]": query = { key.lower(): values for key, values in parse_qs(urlsplit(url).query).items() } algorithm = query.get("x-amz-algorithm", [""])[0] - if algorithm not in ("AWS4-HMAC-SHA256", "AWS4-ECDSA-P256-SHA256"): - return signed_headers + if algorithm not in _AWS_SIGV4_SIGNING_ALGORITHMS: + return set() # presigned requests have SignedHeaders in the URL query. - signed_headers.update( + return { header.lower() for header in query.get("x-amz-signedheaders", [""])[0].split(";") if header - ) - return signed_headers + } def is_valid_sample_rate(rate: "Any", source: str) -> bool: diff --git a/tests/integrations/boto3/test_aws_http_connection.py b/tests/integrations/boto3/test_aws_http_connection.py index 2c61a7b53d..2c3a30103f 100644 --- a/tests/integrations/boto3/test_aws_http_connection.py +++ b/tests/integrations/boto3/test_aws_http_connection.py @@ -8,7 +8,10 @@ import sentry_sdk from sentry_sdk.integrations.stdlib import StdlibIntegration -from sentry_sdk.utils import _get_aws_sigv4_signed_headers +from sentry_sdk.utils import ( + _get_aws_sigv4_signed_headers_from_authorization_header, + _get_aws_sigv4_signed_headers_from_url_query_string, +) @pytest.fixture @@ -124,8 +127,8 @@ def test_aws_http_connection_skips_signed_baggage( assert headers.get_all("baggage") == ["vendor=value"] # preserves existing `sentry-trace` header. assert headers.get_all("sentry-trace") == ["existing-trace"] - assert _get_aws_sigv4_signed_headers( - authorization=headers.get("Authorization", "") + assert _get_aws_sigv4_signed_headers_from_authorization_header( + headers.get("Authorization", "") ) >= { "baggage", "host", @@ -180,7 +183,6 @@ def test_aws_http_connection_skips_query_signed_baggage( sentry_trace_headers = headers.get_all("sentry-trace") assert sentry_trace_headers is not None assert len(sentry_trace_headers) == 1 - assert _get_aws_sigv4_signed_headers( - authorization=headers.get("Authorization", ""), - url=f"http://127.0.0.1:{server.server_port}{path}", + assert _get_aws_sigv4_signed_headers_from_url_query_string( + f"http://127.0.0.1:{server.server_port}{path}" ) >= {"baggage", "host"} diff --git a/tests/integrations/boto3/test_trace_propagation.py b/tests/integrations/boto3/test_trace_propagation.py index 82ba269f6c..b7ed90e8b9 100644 --- a/tests/integrations/boto3/test_trace_propagation.py +++ b/tests/integrations/boto3/test_trace_propagation.py @@ -9,7 +9,10 @@ import sentry_sdk from sentry_sdk.integrations.boto3 import Boto3Integration from sentry_sdk.integrations.stdlib import StdlibIntegration -from sentry_sdk.utils import _get_aws_sigv4_signed_headers +from sentry_sdk.utils import ( + _get_aws_sigv4_signed_headers_from_authorization_header, + _get_aws_sigv4_signed_headers_from_url_query_string, +) class _AwsRequestHandler(BaseHTTPRequestHandler): @@ -114,8 +117,8 @@ def capture_headers_after_instrumentation(request, **kwargs): assert len(sentry_trace_headers) == 1 assert sentry_trace_headers == signed_request_headers["sentry-trace"] # both `baggage` and `sentry-trace` are signed. - signed_headers = _get_aws_sigv4_signed_headers( - authorization=headers.get("Authorization", "") + signed_headers = _get_aws_sigv4_signed_headers_from_authorization_header( + headers.get("Authorization", "") ) assert signed_headers >= {"baggage", "sentry-trace"} finally: @@ -172,8 +175,8 @@ def _inject_signed_baggage(request, **kwargs): assert headers.get_all("baggage") == ["vendor=value"] # `httplib` still adds single `sentry-trace` header. assert len(headers.get_all("sentry-trace")) == 1 - signed_headers = _get_aws_sigv4_signed_headers( - authorization=headers.get("Authorization", "") + signed_headers = _get_aws_sigv4_signed_headers_from_authorization_header( + headers.get("Authorization", "") ) assert "baggage" in signed_headers assert "sentry-trace" not in signed_headers @@ -205,7 +208,7 @@ def test_presigned_urls_do_not_require_sentry_headers(sentry_init): # only `host` header is signed. assert query["X-Amz-SignedHeaders"] == ["host"] - assert _get_aws_sigv4_signed_headers(authorization=None, url=url) == {"host"} + assert _get_aws_sigv4_signed_headers_from_url_query_string(url) == {"host"} # no `sentry-*` or baggage are added. assert "sentry-trace" not in url assert "baggage" not in url