From d4a86033a224e80f2279b702d87f740fbfb3e8b8 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 5 May 2026 13:30:11 +0200 Subject: [PATCH 01/12] fix(stdlib): Instrument response body read for chunked HTTP responses The HTTP client span was being finished in getresponse(), which only waits for response headers. For chunked (or large) responses, the actual data transfer happens later during read(), leaving that time uninstrumented. Defer span completion to HTTPResponse.read() for responses with a body, with HTTPResponse.close() as a safety net for responses that are never read. Responses with no body (Content-Length: 0, HEAD, 204, 304) still finish the span immediately in getresponse(). Co-Authored-By: Claude Opus 4.6 --- sentry_sdk/integrations/stdlib.py | 72 ++++++++++++++----- tests/integrations/stdlib/test_httplib.py | 84 +++++++++++++++++++++++ 2 files changed, 139 insertions(+), 17 deletions(-) diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index 5d8df43eb2..ddf6f5032f 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -2,7 +2,7 @@ import subprocess import sys import platform -from http.client import HTTPConnection +from http.client import HTTPConnection, HTTPResponse import sentry_sdk from sentry_sdk.consts import OP, SPANDATA @@ -66,9 +66,22 @@ def add_python_runtime_context( return event +def _finish_span(span: "Union[Span, StreamedSpan]") -> None: + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_http_request_source(span) + span.end() + else: + span.finish() + with capture_internal_exceptions(): + add_http_request_source(span) + + def _install_httplib() -> None: 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" @@ -172,29 +185,54 @@ def getresponse(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": try: rv = real_getresponse(self, *args, **kwargs) + except BaseException: + _finish_span(span) + raise + + if isinstance(span, StreamedSpan): + status_code = int(rv.status) + span.status = "error" if status_code >= 400 else "ok" + span.set_attribute("http.response.status_code", status_code) + else: + span.set_http_status(int(rv.status)) + span.set_data("reason", rv.reason) - if isinstance(span, StreamedSpan): - status_code = int(rv.status) - span.status = "error" if status_code >= 400 else "ok" - span.set_attribute("http.response.status_code", status_code) - else: - span.set_http_status(int(rv.status)) - span.set_data("reason", rv.reason) + has_body = rv.chunked or (rv.length is not None and rv.length > 0) + if has_body: + rv._sentrysdk_span = span # type: ignore[attr-defined] + else: + _finish_span(span) + + return rv + + def read(self: "HTTPResponse", *args: "Any", **kwargs: "Any") -> "Any": + span = getattr(self, "_sentrysdk_span", None) + + if span is None: + return real_read(self, *args, **kwargs) + + try: + rv = real_read(self, *args, **kwargs) + return rv finally: - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_http_request_source(span) - span.end() - else: - span.finish() + if self.fp is None or self.closed or not rv: + self._sentrysdk_span = None # type: ignore[attr-defined] + _finish_span(span) - with capture_internal_exceptions(): - add_http_request_source(span) + def close(self: "HTTPResponse") -> None: + span = getattr(self, "_sentrysdk_span", None) - return rv + try: + real_close(self) + finally: + if span is not None: + self._sentrysdk_span = None # type: ignore[attr-defined] + _finish_span(span) HTTPConnection.putrequest = putrequest # type: ignore[method-assign] HTTPConnection.getresponse = getresponse # type: ignore[method-assign] + HTTPResponse.read = read # type: ignore[method-assign] + HTTPResponse.close = close # type: ignore[method-assign] def _init_argument( diff --git a/tests/integrations/stdlib/test_httplib.py b/tests/integrations/stdlib/test_httplib.py index 33aa95825d..8c462b9abf 100644 --- a/tests/integrations/stdlib/test_httplib.py +++ b/tests/integrations/stdlib/test_httplib.py @@ -1,6 +1,7 @@ import os import socket import datetime +import time from http.client import HTTPConnection, HTTPSConnection from http.server import BaseHTTPRequestHandler, HTTPServer from socket import SocketIO @@ -1161,3 +1162,86 @@ def test_proxy_http_tunnel( assert span["data"][SPANDATA.HTTP_METHOD] == "GET" assert span["data"][SPANDATA.NETWORK_PEER_ADDRESS] == "localhost" assert span["data"][SPANDATA.NETWORK_PEER_PORT] == PROXY_PORT + + +CHUNK_DELAY = 0.1 +NUM_CHUNKS = 3 + + +class ChunkedResponseHandler(BaseHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + self.send_header("Transfer-Encoding", "chunked") + self.end_headers() + for _ in range(NUM_CHUNKS): + chunk = b"x" * 100 + self.wfile.write(f"{len(chunk):x}\r\n".encode() + chunk + b"\r\n") + self.wfile.flush() + time.sleep(CHUNK_DELAY) + self.wfile.write(b"0\r\n\r\n") + + def log_message(self, *args): + pass + + +def create_chunked_server(): + port = get_free_port() + server = HTTPServer(("localhost", port), ChunkedResponseHandler) + thread = Thread(target=server.serve_forever) + thread.daemon = True + thread.start() + return port + + +CHUNKED_PORT = create_chunked_server() + + +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_chunked_response_span_covers_body_read( + sentry_init, + capture_events, + capture_items, + span_streaming, +): + sentry_init( + traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) + + min_expected_duration = CHUNK_DELAY * NUM_CHUNKS + + if span_streaming: + items = capture_items("span") + + with sentry_sdk.traces.start_span(name="custom parent"): + conn = HTTPConnection("localhost", CHUNKED_PORT) + conn.request("GET", "/chunked") + response = conn.getresponse() + response.read() + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + (span,) = ( + span + for span in spans + if span["attributes"].get("sentry.origin") == "auto.http.stdlib.httplib" + ) + + duration = span["end_timestamp"] - span["start_timestamp"] + assert duration >= min_expected_duration + else: + events = capture_events() + + with start_transaction(name="test_chunked"): + conn = HTTPConnection("localhost", CHUNKED_PORT) + conn.request("GET", "/chunked") + response = conn.getresponse() + response.read() + + (event,) = events + (span,) = event["spans"] + + start = datetime.datetime.fromisoformat(span["start_timestamp"]) + end = datetime.datetime.fromisoformat(span["timestamp"]) + duration = (end - start).total_seconds() + assert duration >= min_expected_duration From f50fbc1c236370b00755260655c61156820ff66a Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 5 May 2026 13:44:23 +0200 Subject: [PATCH 02/12] comments --- sentry_sdk/integrations/stdlib.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index ddf6f5032f..1901fbc5cd 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -197,6 +197,9 @@ def getresponse(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": span.set_http_status(int(rv.status)) span.set_data("reason", rv.reason) + # getresponse doesn't include actually reading the response body. This + # is done in read(). So if the metadata/headers suggest there's a body to + # read, don't finish the span just yet, but save it for ending it later. has_body = rv.chunked or (rv.length is not None and rv.length > 0) if has_body: rv._sentrysdk_span = span # type: ignore[attr-defined] @@ -215,11 +218,16 @@ def read(self: "HTTPResponse", *args: "Any", **kwargs: "Any") -> "Any": rv = real_read(self, *args, **kwargs) return rv finally: + # read() might be called multiple times to consume a single body, + # so we can't just end the span when read() is done. Instead, + # try to figure out whether the response body has been fully read. if self.fp is None or self.closed or not rv: self._sentrysdk_span = None # type: ignore[attr-defined] _finish_span(span) def close(self: "HTTPResponse") -> None: + # We patch close() as a best effort fallback in case the span is not + # ended yet in getresponse() or read(). span = getattr(self, "_sentrysdk_span", None) try: From 3cb3187ad88ceca78889c82f73536eb4d8099f0a Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 5 May 2026 13:46:36 +0200 Subject: [PATCH 03/12] . --- sentry_sdk/integrations/stdlib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index 1901fbc5cd..b6ba86ca13 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -214,6 +214,7 @@ def read(self: "HTTPResponse", *args: "Any", **kwargs: "Any") -> "Any": if span is None: return real_read(self, *args, **kwargs) + rv = None try: rv = real_read(self, *args, **kwargs) return rv From 1391b3fb4820a7fe28c3cb9157c92ca5f7f624ba Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 5 May 2026 13:51:21 +0200 Subject: [PATCH 04/12] fix: Handle pre-3.11 fromisoformat and mypy close() type Co-Authored-By: Claude Opus 4.6 --- sentry_sdk/integrations/stdlib.py | 2 +- tests/integrations/stdlib/test_httplib.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index b6ba86ca13..ca8acf605e 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -241,7 +241,7 @@ def close(self: "HTTPResponse") -> None: HTTPConnection.putrequest = putrequest # type: ignore[method-assign] HTTPConnection.getresponse = getresponse # type: ignore[method-assign] HTTPResponse.read = read # type: ignore[method-assign] - HTTPResponse.close = close # type: ignore[method-assign] + HTTPResponse.close = close # type: ignore[assignment,method-assign] def _init_argument( diff --git a/tests/integrations/stdlib/test_httplib.py b/tests/integrations/stdlib/test_httplib.py index 8c462b9abf..49a2d27d4f 100644 --- a/tests/integrations/stdlib/test_httplib.py +++ b/tests/integrations/stdlib/test_httplib.py @@ -1241,7 +1241,9 @@ def test_chunked_response_span_covers_body_read( (event,) = events (span,) = event["spans"] - start = datetime.datetime.fromisoformat(span["start_timestamp"]) - end = datetime.datetime.fromisoformat(span["timestamp"]) + start = datetime.datetime.fromisoformat( + span["start_timestamp"].replace("Z", "+00:00") + ) + end = datetime.datetime.fromisoformat(span["timestamp"].replace("Z", "+00:00")) duration = (end - start).total_seconds() assert duration >= min_expected_duration From bc404b6871448612f8248bb15fa8da5b1ffef1c6 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 5 May 2026 13:59:51 +0200 Subject: [PATCH 05/12] fix: Use strptime instead of fromisoformat for Python 3.6 compat Co-Authored-By: Claude Opus 4.6 --- tests/integrations/stdlib/test_httplib.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/integrations/stdlib/test_httplib.py b/tests/integrations/stdlib/test_httplib.py index 49a2d27d4f..3b1b61d1de 100644 --- a/tests/integrations/stdlib/test_httplib.py +++ b/tests/integrations/stdlib/test_httplib.py @@ -1241,9 +1241,8 @@ def test_chunked_response_span_covers_body_read( (event,) = events (span,) = event["spans"] - start = datetime.datetime.fromisoformat( - span["start_timestamp"].replace("Z", "+00:00") - ) - end = datetime.datetime.fromisoformat(span["timestamp"].replace("Z", "+00:00")) + fmt = "%Y-%m-%dT%H:%M:%S.%fZ" + start = datetime.datetime.strptime(span["start_timestamp"], fmt) + end = datetime.datetime.strptime(span["timestamp"], fmt) duration = (end - start).total_seconds() assert duration >= min_expected_duration From 03084087f9c986223b74048404246df7c249686b Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 5 May 2026 14:04:53 +0200 Subject: [PATCH 06/12] fix: Don't finish span prematurely on read(0) Drop the `not rv` check from the read() wrapper's span-finishing condition. read(0) legitimately returns b"" without consuming the body, which would falsely trigger span completion. The fp and closed checks are sufficient to detect when the body is fully consumed. Co-Authored-By: Claude Opus 4.6 --- sentry_sdk/integrations/stdlib.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index ca8acf605e..06f6c596ad 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -214,15 +214,13 @@ def read(self: "HTTPResponse", *args: "Any", **kwargs: "Any") -> "Any": if span is None: return real_read(self, *args, **kwargs) - rv = None try: - rv = real_read(self, *args, **kwargs) - return rv + return real_read(self, *args, **kwargs) finally: # read() might be called multiple times to consume a single body, # so we can't just end the span when read() is done. Instead, # try to figure out whether the response body has been fully read. - if self.fp is None or self.closed or not rv: + if self.fp is None or self.closed: self._sentrysdk_span = None # type: ignore[attr-defined] _finish_span(span) From 5fe408b7a13c1e99b2994f1457910b09ebbe6edf Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 5 May 2026 14:09:09 +0200 Subject: [PATCH 07/12] refactor: Move chunked server setup to top of test file Co-Authored-By: Claude Opus 4.6 --- tests/integrations/stdlib/test_httplib.py | 63 +++++++++++------------ 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/tests/integrations/stdlib/test_httplib.py b/tests/integrations/stdlib/test_httplib.py index 3b1b61d1de..2cb407bd6f 100644 --- a/tests/integrations/stdlib/test_httplib.py +++ b/tests/integrations/stdlib/test_httplib.py @@ -45,6 +45,37 @@ def create_mock_proxy_server(): PROXY_PORT = create_mock_proxy_server() +CHUNK_DELAY = 0.1 +NUM_CHUNKS = 3 + + +class ChunkedResponseHandler(BaseHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + self.send_header("Transfer-Encoding", "chunked") + self.end_headers() + for _ in range(NUM_CHUNKS): + chunk = b"x" * 100 + self.wfile.write(f"{len(chunk):x}\r\n".encode() + chunk + b"\r\n") + self.wfile.flush() + time.sleep(CHUNK_DELAY) + self.wfile.write(b"0\r\n\r\n") + + def log_message(self, *args): + pass + + +def create_chunked_server(): + port = get_free_port() + server = HTTPServer(("localhost", port), ChunkedResponseHandler) + thread = Thread(target=server.serve_forever) + thread.daemon = True + thread.start() + return port + + +CHUNKED_PORT = create_chunked_server() + def test_crumb_capture(sentry_init, capture_events): sentry_init(integrations=[StdlibIntegration()]) @@ -1164,38 +1195,6 @@ def test_proxy_http_tunnel( assert span["data"][SPANDATA.NETWORK_PEER_PORT] == PROXY_PORT -CHUNK_DELAY = 0.1 -NUM_CHUNKS = 3 - - -class ChunkedResponseHandler(BaseHTTPRequestHandler): - def do_GET(self): - self.send_response(200) - self.send_header("Transfer-Encoding", "chunked") - self.end_headers() - for _ in range(NUM_CHUNKS): - chunk = b"x" * 100 - self.wfile.write(f"{len(chunk):x}\r\n".encode() + chunk + b"\r\n") - self.wfile.flush() - time.sleep(CHUNK_DELAY) - self.wfile.write(b"0\r\n\r\n") - - def log_message(self, *args): - pass - - -def create_chunked_server(): - port = get_free_port() - server = HTTPServer(("localhost", port), ChunkedResponseHandler) - thread = Thread(target=server.serve_forever) - thread.daemon = True - thread.start() - return port - - -CHUNKED_PORT = create_chunked_server() - - @pytest.mark.parametrize("span_streaming", [True, False]) def test_chunked_response_span_covers_body_read( sentry_init, From c7e452c1ea882512d312c069087619d141c4b275 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 6 May 2026 09:39:07 +0200 Subject: [PATCH 08/12] rename helper --- sentry_sdk/integrations/stdlib.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index 06f6c596ad..f3e512ae95 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -66,7 +66,7 @@ def add_python_runtime_context( return event -def _finish_span(span: "Union[Span, StreamedSpan]") -> None: +def _complete_span(span: "Union[Span, StreamedSpan]") -> None: if isinstance(span, StreamedSpan): with capture_internal_exceptions(): add_http_request_source(span) @@ -186,7 +186,7 @@ def getresponse(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": try: rv = real_getresponse(self, *args, **kwargs) except BaseException: - _finish_span(span) + _complete_span(span) raise if isinstance(span, StreamedSpan): @@ -204,7 +204,7 @@ def getresponse(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": if has_body: rv._sentrysdk_span = span # type: ignore[attr-defined] else: - _finish_span(span) + _complete_span(span) return rv @@ -222,7 +222,7 @@ def read(self: "HTTPResponse", *args: "Any", **kwargs: "Any") -> "Any": # try to figure out whether the response body has been fully read. if self.fp is None or self.closed: self._sentrysdk_span = None # type: ignore[attr-defined] - _finish_span(span) + _complete_span(span) def close(self: "HTTPResponse") -> None: # We patch close() as a best effort fallback in case the span is not @@ -234,7 +234,7 @@ def close(self: "HTTPResponse") -> None: finally: if span is not None: self._sentrysdk_span = None # type: ignore[attr-defined] - _finish_span(span) + _complete_span(span) HTTPConnection.putrequest = putrequest # type: ignore[method-assign] HTTPConnection.getresponse = getresponse # type: ignore[method-assign] From 0296b8018e8ca2f51e02e9ecf8441860a6da141a Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 6 May 2026 10:00:13 +0200 Subject: [PATCH 09/12] refactor: Unpack spans directly instead of filtering Co-Authored-By: Claude Opus 4.6 --- tests/integrations/stdlib/test_httplib.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/integrations/stdlib/test_httplib.py b/tests/integrations/stdlib/test_httplib.py index 2cb407bd6f..589a8e8e97 100644 --- a/tests/integrations/stdlib/test_httplib.py +++ b/tests/integrations/stdlib/test_httplib.py @@ -1219,14 +1219,9 @@ def test_chunked_response_span_covers_body_read( response.read() sentry_sdk.flush() - spans = [item.payload for item in items if item.type == "span"] - (span,) = ( - span - for span in spans - if span["attributes"].get("sentry.origin") == "auto.http.stdlib.httplib" - ) + http_span, parent_span = [item.payload for item in items] - duration = span["end_timestamp"] - span["start_timestamp"] + duration = http_span["end_timestamp"] - http_span["start_timestamp"] assert duration >= min_expected_duration else: events = capture_events() From 2c7078a046fe7253772bbf7933bf179d18fc9679 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 6 May 2026 10:08:43 +0200 Subject: [PATCH 10/12] Apply suggestion Co-authored-by: Alex Alderman Webb --- sentry_sdk/integrations/stdlib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index f3e512ae95..bb0442798b 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -227,11 +227,11 @@ def read(self: "HTTPResponse", *args: "Any", **kwargs: "Any") -> "Any": def close(self: "HTTPResponse") -> None: # We patch close() as a best effort fallback in case the span is not # ended yet in getresponse() or read(). - span = getattr(self, "_sentrysdk_span", None) - + try: real_close(self) finally: + span = getattr(self, "_sentrysdk_span", None) if span is not None: self._sentrysdk_span = None # type: ignore[attr-defined] _complete_span(span) From 5e768d8529ce30f46ec8f679c0c158a4835d7406 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 6 May 2026 10:13:45 +0200 Subject: [PATCH 11/12] move span handling to finally --- sentry_sdk/integrations/stdlib.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index bb0442798b..c0e1fe065e 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -209,18 +209,14 @@ def getresponse(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": return rv def read(self: "HTTPResponse", *args: "Any", **kwargs: "Any") -> "Any": - span = getattr(self, "_sentrysdk_span", None) - - if span is None: - return real_read(self, *args, **kwargs) - try: return real_read(self, *args, **kwargs) finally: + span = getattr(self, "_sentrysdk_span", None) # read() might be called multiple times to consume a single body, # so we can't just end the span when read() is done. Instead, # try to figure out whether the response body has been fully read. - if self.fp is None or self.closed: + if span and self.fp is None or self.closed: self._sentrysdk_span = None # type: ignore[attr-defined] _complete_span(span) From 499e036067e493c51a07f2e6c573e70729d9fe6e Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 6 May 2026 10:22:20 +0200 Subject: [PATCH 12/12] i can def write basic boolean logic --- sentry_sdk/integrations/stdlib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index c0e1fe065e..7573f8da7c 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -216,14 +216,14 @@ def read(self: "HTTPResponse", *args: "Any", **kwargs: "Any") -> "Any": # read() might be called multiple times to consume a single body, # so we can't just end the span when read() is done. Instead, # try to figure out whether the response body has been fully read. - if span and self.fp is None or self.closed: + if span and (self.fp is None or self.closed): self._sentrysdk_span = None # type: ignore[attr-defined] _complete_span(span) def close(self: "HTTPResponse") -> None: # We patch close() as a best effort fallback in case the span is not # ended yet in getresponse() or read(). - + try: real_close(self) finally: