From a169cef66ad9c16ddfa3d3a76a3e07eb4345e21a Mon Sep 17 00:00:00 2001 From: antisch Date: Wed, 1 Oct 2025 10:22:13 +1300 Subject: [PATCH 1/9] Support timeout error in requests transport --- .../pipeline/transport/_requests_basic.py | 11 ++--- .../azure-core/tests/test_basic_transport.py | 43 ++++++++++++++++++- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/sdk/core/azure-core/azure/core/pipeline/transport/_requests_basic.py b/sdk/core/azure-core/azure/core/pipeline/transport/_requests_basic.py index 00637cfd59db..a7c41119af1a 100644 --- a/sdk/core/azure-core/azure/core/pipeline/transport/_requests_basic.py +++ b/sdk/core/azure-core/azure/core/pipeline/transport/_requests_basic.py @@ -46,7 +46,9 @@ from azure.core.configuration import ConnectionConfiguration from azure.core.exceptions import ( ServiceRequestError, + ServiceRequestTimeoutError, ServiceResponseError, + ServiceResponseTimeoutError, IncompleteReadError, HttpResponseError, DecodeError, @@ -384,13 +386,12 @@ def send( # pylint: disable=too-many-statements "Please report this issue to https://github.com/Azure/azure-sdk-for-python/issues." ) from err raise - except ( - NewConnectionError, - ConnectTimeoutError, - ) as err: + except NewConnectionError as err: error = ServiceRequestError(err, error=err) + except ConnectTimeoutError as err: + error = ServiceRequestTimeoutError(err, error=err) except requests.exceptions.ReadTimeout as err: - error = ServiceResponseError(err, error=err) + error = ServiceResponseTimeoutError(err, error=err) except requests.exceptions.ConnectionError as err: if err.args and isinstance(err.args[0], ProtocolError): error = ServiceResponseError(err, error=err) diff --git a/sdk/core/azure-core/tests/test_basic_transport.py b/sdk/core/azure-core/tests/test_basic_transport.py index 2e75b93d2a96..4712e907c784 100644 --- a/sdk/core/azure-core/tests/test_basic_transport.py +++ b/sdk/core/azure-core/tests/test_basic_transport.py @@ -16,7 +16,13 @@ from azure.core.pipeline.transport._base import HttpTransport, _deserialize_response, _urljoin from azure.core.pipeline.policies import HeadersPolicy from azure.core.pipeline import Pipeline -from azure.core.exceptions import HttpResponseError +from azure.core.exceptions import ( + HttpResponseError, + ServiceRequestError, + ServiceResponseError, + ServiceRequestTimeoutError, + ServiceResponseTimeoutError, +) import logging import pytest from utils import ( @@ -1322,3 +1328,38 @@ def test_close_too_soon_works_fine(caplog, port, http_request): result = transport.send(request) assert result # No exception is good enough here + + +@pytest.mark.parametrize("http_request", HTTP_REQUESTS) +async def test_requests_timeout_response(caplog, port, http_request): + transport = RequestsTransport() + + request = http_request("GET", f"http://localhost:{port}/basic/string") + + with pytest.raises(ServiceResponseTimeoutError) as err: + transport.send(request, read_timeout=0.0001) + + with pytest.raises(ServiceResponseError) as err: + transport.send(request, read_timeout=0.0001) + + stream_request = http_request("GET", f"http://localhost:{port}/streams/basic") + with pytest.raises(ServiceResponseTimeoutError) as err: + transport.send(stream_request, stream=True, read_timeout=0.0001) + + +@pytest.mark.parametrize("http_request", HTTP_REQUESTS) +@pytest.mark.asyncio +async def test_requests_timeout_request(caplog, port, http_request): + transport = RequestsTransport() + + request = http_request("GET", f"http://localhost:{port}/basic/string") + + with pytest.raises(ServiceRequestError) as err: + transport.send(request, connection_timeout=0.0001) + + with pytest.raises(ServiceRequestTimeoutError) as err: + transport.send(request, connection_timeout=0.0001) + + stream_request = http_request("GET", f"http://localhost:{port}/streams/basic") + with pytest.raises(ServiceRequestTimeoutError) as err: + transport.send(stream_request, stream=True, connection_timeout=0.0001) From cde45b9ce7f53154a1890d3c2a7c13740889864d Mon Sep 17 00:00:00 2001 From: antisch Date: Wed, 8 Oct 2025 11:56:09 +1300 Subject: [PATCH 2/9] Requests transport error updates --- .../azure-core/azure/core/pipeline/_tools.py | 4 +- .../azure/core/pipeline/_tools_async.py | 4 +- .../pipeline/transport/_requests_basic.py | 14 ++++- .../azure-core/tests/test_basic_transport.py | 58 ++++++++++++------- 4 files changed, 50 insertions(+), 30 deletions(-) diff --git a/sdk/core/azure-core/azure/core/pipeline/_tools.py b/sdk/core/azure-core/azure/core/pipeline/_tools.py index 530d008aa209..789d21b4ce79 100644 --- a/sdk/core/azure-core/azure/core/pipeline/_tools.py +++ b/sdk/core/azure-core/azure/core/pipeline/_tools.py @@ -80,7 +80,5 @@ def handle_non_stream_rest_response(response: HttpResponse) -> None: """ try: response.read() + finally: response.close() - except Exception as exc: - response.close() - raise exc diff --git a/sdk/core/azure-core/azure/core/pipeline/_tools_async.py b/sdk/core/azure-core/azure/core/pipeline/_tools_async.py index bc23c202eaea..7c0d201fd61d 100644 --- a/sdk/core/azure-core/azure/core/pipeline/_tools_async.py +++ b/sdk/core/azure-core/azure/core/pipeline/_tools_async.py @@ -67,7 +67,5 @@ async def handle_no_stream_rest_response(response: "RestAsyncHttpResponse") -> N """ try: await response.read() + finally: await response.close() - except Exception as exc: - await response.close() - raise exc diff --git a/sdk/core/azure-core/azure/core/pipeline/transport/_requests_basic.py b/sdk/core/azure-core/azure/core/pipeline/transport/_requests_basic.py index a7c41119af1a..9f102f4b0b20 100644 --- a/sdk/core/azure-core/azure/core/pipeline/transport/_requests_basic.py +++ b/sdk/core/azure-core/azure/core/pipeline/transport/_requests_basic.py @@ -87,7 +87,7 @@ def _read_raw_stream(response, chunk_size=1): except CoreDecodeError as e: raise DecodeError(e, error=e) from e except ReadTimeoutError as e: - raise ServiceRequestError(e, error=e) from e + raise ServiceResponseTimeoutError(e, error=e) from e else: # Standard file-like object. while True: @@ -204,6 +204,14 @@ def __next__(self): _LOGGER.warning("Unable to stream download.") internal_response.close() raise HttpResponseError(err, error=err) from err + except requests.ConnectionError as err: + internal_response.close() + if err.args and isinstance(err.args[0], ReadTimeoutError): + raise ServiceResponseTimeoutError(err, error=err) from err + raise ServiceResponseError(err, error=err) from err + except requests.RequestException as err: + internal_response.close() + raise ServiceResponseError(err, error=err) from err except Exception as err: _LOGGER.warning("Unable to stream download.") internal_response.close() @@ -390,6 +398,8 @@ def send( # pylint: disable=too-many-statements error = ServiceRequestError(err, error=err) except ConnectTimeoutError as err: error = ServiceRequestTimeoutError(err, error=err) + except requests.exceptions.ConnectTimeout as err: + error = ServiceRequestTimeoutError(err, error=err) except requests.exceptions.ReadTimeout as err: error = ServiceResponseTimeoutError(err, error=err) except requests.exceptions.ConnectionError as err: @@ -406,7 +416,7 @@ def send( # pylint: disable=too-many-statements _LOGGER.warning("Unable to stream download.") error = HttpResponseError(err, error=err) except requests.RequestException as err: - error = ServiceRequestError(err, error=err) + error = ServiceResponseError(err, error=err) if error: raise error diff --git a/sdk/core/azure-core/tests/test_basic_transport.py b/sdk/core/azure-core/tests/test_basic_transport.py index 4712e907c784..3e75e05c20fd 100644 --- a/sdk/core/azure-core/tests/test_basic_transport.py +++ b/sdk/core/azure-core/tests/test_basic_transport.py @@ -6,12 +6,19 @@ from http.client import HTTPConnection from collections import OrderedDict import sys +import logging +import pytest try: from unittest import mock except ImportError: import mock +from urllib3.util import connection +from urllib3.response import HTTPResponse +from urllib3.connection import HTTPConnection +from socket import timeout as SocketTimeout + from azure.core.pipeline.transport import HttpResponse as PipelineTransportHttpResponse, RequestsTransport from azure.core.pipeline.transport._base import HttpTransport, _deserialize_response, _urljoin from azure.core.pipeline.policies import HeadersPolicy @@ -23,8 +30,7 @@ ServiceRequestTimeoutError, ServiceResponseTimeoutError, ) -import logging -import pytest + from utils import ( HTTP_REQUESTS, request_and_responses_product, @@ -1331,35 +1337,43 @@ def test_close_too_soon_works_fine(caplog, port, http_request): @pytest.mark.parametrize("http_request", HTTP_REQUESTS) -async def test_requests_timeout_response(caplog, port, http_request): +def test_requests_timeout_response(caplog, port, http_request): transport = RequestsTransport() request = http_request("GET", f"http://localhost:{port}/basic/string") - with pytest.raises(ServiceResponseTimeoutError) as err: - transport.send(request, read_timeout=0.0001) - - with pytest.raises(ServiceResponseError) as err: - transport.send(request, read_timeout=0.0001) - - stream_request = http_request("GET", f"http://localhost:{port}/streams/basic") - with pytest.raises(ServiceResponseTimeoutError) as err: - transport.send(stream_request, stream=True, read_timeout=0.0001) - + with mock.patch.object(HTTPConnection, "getresponse", side_effect=SocketTimeout) as mock_method: + with pytest.raises(ServiceResponseTimeoutError) as err: + transport.send(request, read_timeout=0.0001) + + with pytest.raises(ServiceResponseError) as err: + transport.send(request, read_timeout=0.0001) + + stream_request = http_request("GET", f"http://localhost:{port}/streams/basic") + with pytest.raises(ServiceResponseTimeoutError) as err: + transport.send(stream_request, stream=True, read_timeout=0.0001) + + stream_resp = transport.send(stream_request, stream=True) + with mock.patch.object(HTTPResponse, "_handle_chunk", side_effect=SocketTimeout) as mock_method: + with pytest.raises(ServiceResponseTimeoutError) as err: + try: + stream_resp.read() + except AttributeError: + b"".join(stream_resp.stream_download(None)) @pytest.mark.parametrize("http_request", HTTP_REQUESTS) -@pytest.mark.asyncio -async def test_requests_timeout_request(caplog, port, http_request): +def test_requests_timeout_request(caplog, port, http_request): transport = RequestsTransport() request = http_request("GET", f"http://localhost:{port}/basic/string") - with pytest.raises(ServiceRequestError) as err: - transport.send(request, connection_timeout=0.0001) + with mock.patch.object(connection, 'create_connection', side_effect=SocketTimeout) as mock_method: + with pytest.raises(ServiceRequestTimeoutError) as err: + transport.send(request, connection_timeout=0.0001) - with pytest.raises(ServiceRequestTimeoutError) as err: - transport.send(request, connection_timeout=0.0001) + with pytest.raises(ServiceRequestTimeoutError) as err: + transport.send(request, connection_timeout=0.0001) - stream_request = http_request("GET", f"http://localhost:{port}/streams/basic") - with pytest.raises(ServiceRequestTimeoutError) as err: - transport.send(stream_request, stream=True, connection_timeout=0.0001) + stream_request = http_request("GET", f"http://localhost:{port}/streams/basic") + with pytest.raises(ServiceRequestTimeoutError) as err: + transport.send(stream_request, stream=True, connection_timeout=0.0001) From c5711e0d2ad5b1ee4c9c24f84e971255828de337 Mon Sep 17 00:00:00 2001 From: antisch Date: Mon, 13 Oct 2025 15:51:00 +1300 Subject: [PATCH 3/9] Test updates --- sdk/core/azure-core/tests/test_stream_generator.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sdk/core/azure-core/tests/test_stream_generator.py b/sdk/core/azure-core/tests/test_stream_generator.py index eeacb47f077c..e9ea512d0942 100644 --- a/sdk/core/azure-core/tests/test_stream_generator.py +++ b/sdk/core/azure-core/tests/test_stream_generator.py @@ -9,6 +9,7 @@ ) from azure.core.pipeline import Pipeline, PipelineResponse from azure.core.pipeline.transport._requests_basic import StreamDownloadGenerator +from azure.core.exceptions import ServiceResponseError try: from unittest import mock @@ -73,7 +74,7 @@ def close(self): http_response.internal_response = MockInternalResponse() stream = StreamDownloadGenerator(pipeline, http_response, decompress=False) with mock.patch("time.sleep", return_value=None): - with pytest.raises(requests.exceptions.ConnectionError): + with pytest.raises(ServiceResponseError): stream.__next__() @@ -133,5 +134,5 @@ def mock_run(self, *args, **kwargs): pipeline = Pipeline(transport) pipeline.run = mock_run downloader = response.stream_download(pipeline, decompress=False) - with pytest.raises(requests.exceptions.ConnectionError): + with pytest.raises(ServiceResponseError): full_response = b"".join(downloader) From 949f4acb4028c2a65a6ef8079313b8ccfd5a0d33 Mon Sep 17 00:00:00 2001 From: antisch Date: Tue, 14 Oct 2025 13:12:41 +1300 Subject: [PATCH 4/9] Fix imports --- sdk/core/azure-core/tests/test_basic_transport.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/sdk/core/azure-core/tests/test_basic_transport.py b/sdk/core/azure-core/tests/test_basic_transport.py index 3e75e05c20fd..50d0d63340a8 100644 --- a/sdk/core/azure-core/tests/test_basic_transport.py +++ b/sdk/core/azure-core/tests/test_basic_transport.py @@ -14,9 +14,9 @@ except ImportError: import mock -from urllib3.util import connection -from urllib3.response import HTTPResponse -from urllib3.connection import HTTPConnection +from urllib3.util import connection as urllib_connection +from urllib3.response import HTTPResponse as UrllibResponse +from urllib3.connection import HTTPConnection as UrllibConnection from socket import timeout as SocketTimeout from azure.core.pipeline.transport import HttpResponse as PipelineTransportHttpResponse, RequestsTransport @@ -1342,7 +1342,7 @@ def test_requests_timeout_response(caplog, port, http_request): request = http_request("GET", f"http://localhost:{port}/basic/string") - with mock.patch.object(HTTPConnection, "getresponse", side_effect=SocketTimeout) as mock_method: + with mock.patch.object(UrllibConnection, "getresponse", side_effect=SocketTimeout) as mock_method: with pytest.raises(ServiceResponseTimeoutError) as err: transport.send(request, read_timeout=0.0001) @@ -1354,11 +1354,13 @@ def test_requests_timeout_response(caplog, port, http_request): transport.send(stream_request, stream=True, read_timeout=0.0001) stream_resp = transport.send(stream_request, stream=True) - with mock.patch.object(HTTPResponse, "_handle_chunk", side_effect=SocketTimeout) as mock_method: + with mock.patch.object(UrllibResponse, "_handle_chunk", side_effect=SocketTimeout) as mock_method: with pytest.raises(ServiceResponseTimeoutError) as err: try: + # current HttpResponse stream_resp.read() except AttributeError: + # legacy HttpResponse b"".join(stream_resp.stream_download(None)) @pytest.mark.parametrize("http_request", HTTP_REQUESTS) @@ -1367,7 +1369,7 @@ def test_requests_timeout_request(caplog, port, http_request): request = http_request("GET", f"http://localhost:{port}/basic/string") - with mock.patch.object(connection, 'create_connection', side_effect=SocketTimeout) as mock_method: + with mock.patch.object(urllib_connection, 'create_connection', side_effect=SocketTimeout) as mock_method: with pytest.raises(ServiceRequestTimeoutError) as err: transport.send(request, connection_timeout=0.0001) From 5301a11913b673df8c938d1fb2ab441f1e9dc463 Mon Sep 17 00:00:00 2001 From: antisch Date: Mon, 20 Oct 2025 10:13:43 +1300 Subject: [PATCH 5/9] Fix black --- sdk/core/azure-core/tests/test_basic_transport.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sdk/core/azure-core/tests/test_basic_transport.py b/sdk/core/azure-core/tests/test_basic_transport.py index 50d0d63340a8..3ce861ba34e4 100644 --- a/sdk/core/azure-core/tests/test_basic_transport.py +++ b/sdk/core/azure-core/tests/test_basic_transport.py @@ -1352,7 +1352,7 @@ def test_requests_timeout_response(caplog, port, http_request): stream_request = http_request("GET", f"http://localhost:{port}/streams/basic") with pytest.raises(ServiceResponseTimeoutError) as err: transport.send(stream_request, stream=True, read_timeout=0.0001) - + stream_resp = transport.send(stream_request, stream=True) with mock.patch.object(UrllibResponse, "_handle_chunk", side_effect=SocketTimeout) as mock_method: with pytest.raises(ServiceResponseTimeoutError) as err: @@ -1363,13 +1363,14 @@ def test_requests_timeout_response(caplog, port, http_request): # legacy HttpResponse b"".join(stream_resp.stream_download(None)) + @pytest.mark.parametrize("http_request", HTTP_REQUESTS) def test_requests_timeout_request(caplog, port, http_request): transport = RequestsTransport() request = http_request("GET", f"http://localhost:{port}/basic/string") - with mock.patch.object(urllib_connection, 'create_connection', side_effect=SocketTimeout) as mock_method: + with mock.patch.object(urllib_connection, "create_connection", side_effect=SocketTimeout) as mock_method: with pytest.raises(ServiceRequestTimeoutError) as err: transport.send(request, connection_timeout=0.0001) From 37cf4a5fbf57e5b3888689c7cf43f100d36f36b9 Mon Sep 17 00:00:00 2001 From: antisch Date: Mon, 20 Oct 2025 11:37:28 +1300 Subject: [PATCH 6/9] aiohttp fixes --- .../azure/core/pipeline/transport/_aiohttp.py | 4 +- .../azure-core/azure/core/rest/_aiohttp.py | 3 +- .../async_tests/test_basic_transport_async.py | 78 ++++++++++++------- .../azure-core/tests/test_basic_transport.py | 13 +--- 4 files changed, 56 insertions(+), 42 deletions(-) diff --git a/sdk/core/azure-core/azure/core/pipeline/transport/_aiohttp.py b/sdk/core/azure-core/azure/core/pipeline/transport/_aiohttp.py index 64ce963a1229..9c653b34d55b 100644 --- a/sdk/core/azure-core/azure/core/pipeline/transport/_aiohttp.py +++ b/sdk/core/azure-core/azure/core/pipeline/transport/_aiohttp.py @@ -474,7 +474,7 @@ async def __anext__(self): except aiohttp.client_exceptions.ClientResponseError as err: raise ServiceResponseError(err, error=err) from err except asyncio.TimeoutError as err: - raise ServiceResponseError(err, error=err) from err + raise ServiceResponseTimeoutError(err, error=err) from err except aiohttp.client_exceptions.ClientError as err: raise ServiceRequestError(err, error=err) from err except Exception as err: @@ -571,7 +571,7 @@ async def load_body(self) -> None: except aiohttp.client_exceptions.ClientResponseError as err: raise ServiceResponseError(err, error=err) from err except asyncio.TimeoutError as err: - raise ServiceResponseError(err, error=err) from err + raise ServiceResponseTimeoutError(err, error=err) from err except aiohttp.client_exceptions.ClientError as err: raise ServiceRequestError(err, error=err) from err diff --git a/sdk/core/azure-core/azure/core/rest/_aiohttp.py b/sdk/core/azure-core/azure/core/rest/_aiohttp.py index f173a8957456..5b7f6b0934a0 100644 --- a/sdk/core/azure-core/azure/core/rest/_aiohttp.py +++ b/sdk/core/azure-core/azure/core/rest/_aiohttp.py @@ -211,8 +211,7 @@ async def read(self) -> bytes: :rtype: bytes """ if not self._content: - self._stream_download_check() - self._content = await self._internal_response.read() + self._content = await super().read() await self._set_read_checks() return _aiohttp_body_helper(self) diff --git a/sdk/core/azure-core/tests/async_tests/test_basic_transport_async.py b/sdk/core/azure-core/tests/async_tests/test_basic_transport_async.py index f0e11b1d213c..dfa0090ac450 100644 --- a/sdk/core/azure-core/tests/async_tests/test_basic_transport_async.py +++ b/sdk/core/azure-core/tests/async_tests/test_basic_transport_async.py @@ -3,6 +3,15 @@ # Licensed under the MIT License. See LICENSE.txt in the project root for # license information. # ------------------------------------------------------------------------- + +import pytest +import sys +import asyncio +from packaging.version import Version +from unittest import mock + +import aiohttp + from azure.core.pipeline.transport import ( AsyncHttpResponse as PipelineTransportAsyncHttpResponse, AsyncHttpTransport, @@ -21,13 +30,8 @@ ServiceRequestTimeoutError, ServiceResponseTimeoutError, ) + from utils import HTTP_REQUESTS, request_and_responses_product -import pytest -import sys -import asyncio -from unittest.mock import Mock -from packaging.version import Version -import aiohttp # transport = mock.MagicMock(spec=AsyncHttpTransport) @@ -1049,47 +1053,63 @@ async def test_close_too_soon_works_fine(caplog, port, http_request): assert result # No exception is good enough here -@pytest.mark.skipif( - Version(aiohttp.__version__) >= Version("3.10"), - reason="aiohttp 3.10 introduced separate connection timeout", -) @pytest.mark.parametrize("http_request", HTTP_REQUESTS) @pytest.mark.asyncio -async def test_aiohttp_timeout_response(http_request): +async def test_aiohttp_timeout_response(port, http_request): async with AioHttpTransport() as transport: - transport.session._connector.connect = Mock(side_effect=asyncio.TimeoutError("Too slow!")) - request = http_request("GET", f"http://localhost:12345/basic/string") + request = http_request("GET", f"http://localhost:{port}/basic/string") with pytest.raises(ServiceResponseTimeoutError) as err: - await transport.send(request) + await transport.send(request, read_timeout=0.0001) with pytest.raises(ServiceResponseError) as err: - await transport.send(request) + await transport.send(request, read_timeout=0.0001) - stream_request = http_request("GET", f"http://localhost:12345/streams/basic") + stream_resp = http_request("GET", f"http://localhost:{port}/streams/basic") with pytest.raises(ServiceResponseTimeoutError) as err: - await transport.send(stream_request, stream=True) + await transport.send(stream_resp, stream=True, read_timeout=0.0001) + + stream_resp = await transport.send(stream_resp, stream=True) + with mock.patch.object( + aiohttp.streams.StreamReader, "read", side_effect=asyncio.TimeoutError("Too slow!") + ) as mock_method: + with pytest.raises(ServiceResponseTimeoutError) as err: + try: + # current HttpResponse + await stream_resp.read() + except AttributeError: + # legacy HttpResponse + b"".join([b async for b in stream_resp.stream_download(None)]) -@pytest.mark.skipif( - Version(aiohttp.__version__) < Version("3.10"), - reason="aiohttp 3.10 introduced separate connection timeout", -) @pytest.mark.parametrize("http_request", HTTP_REQUESTS) @pytest.mark.asyncio async def test_aiohttp_timeout_request(http_request): async with AioHttpTransport() as transport: - transport.session._connector.connect = Mock(side_effect=asyncio.TimeoutError("Too slow!")) + transport.session._connector.connect = mock.Mock(side_effect=asyncio.TimeoutError("Too slow!")) request = http_request("GET", f"http://localhost:12345/basic/string") - with pytest.raises(ServiceRequestTimeoutError) as err: - await transport.send(request) + # aiohttp 3.10 introduced separate connection timeout + if Version(aiohttp.__version__) >= Version("3.10"): + with pytest.raises(ServiceRequestTimeoutError) as err: + await transport.send(request) + + with pytest.raises(ServiceRequestError) as err: + await transport.send(request) + + stream_request = http_request("GET", f"http://localhost:12345/streams/basic") + with pytest.raises(ServiceRequestTimeoutError) as err: + await transport.send(stream_request, stream=True) + + else: + with pytest.raises(ServiceResponseTimeoutError) as err: + await transport.send(request) - with pytest.raises(ServiceRequestError) as err: - await transport.send(request) + with pytest.raises(ServiceResponseError) as err: + await transport.send(request) - stream_request = http_request("GET", f"http://localhost:12345/streams/basic") - with pytest.raises(ServiceRequestTimeoutError) as err: - await transport.send(stream_request, stream=True) + stream_request = http_request("GET", f"http://localhost:12345/streams/basic") + with pytest.raises(ServiceResponseTimeoutError) as err: + await transport.send(stream_request, stream=True) diff --git a/sdk/core/azure-core/tests/test_basic_transport.py b/sdk/core/azure-core/tests/test_basic_transport.py index 3ce861ba34e4..24d3790f0590 100644 --- a/sdk/core/azure-core/tests/test_basic_transport.py +++ b/sdk/core/azure-core/tests/test_basic_transport.py @@ -5,20 +5,17 @@ # ------------------------------------------------------------------------- from http.client import HTTPConnection from collections import OrderedDict -import sys import logging import pytest - -try: - from unittest import mock -except ImportError: - import mock +from unittest import mock +from socket import timeout as SocketTimeout from urllib3.util import connection as urllib_connection from urllib3.response import HTTPResponse as UrllibResponse from urllib3.connection import HTTPConnection as UrllibConnection -from socket import timeout as SocketTimeout +from azure.core.rest._http_response_impl import HttpResponseImpl as RestHttpResponseImpl +from azure.core.pipeline._tools import is_rest from azure.core.pipeline.transport import HttpResponse as PipelineTransportHttpResponse, RequestsTransport from azure.core.pipeline.transport._base import HttpTransport, _deserialize_response, _urljoin from azure.core.pipeline.policies import HeadersPolicy @@ -37,8 +34,6 @@ HTTP_CLIENT_TRANSPORT_RESPONSES, create_transport_response, ) -from azure.core.rest._http_response_impl import HttpResponseImpl as RestHttpResponseImpl -from azure.core.pipeline._tools import is_rest class PipelineTransportMockResponse(PipelineTransportHttpResponse): From 0f03858b8d378366cfee075bbd88f029360150db Mon Sep 17 00:00:00 2001 From: antisch Date: Mon, 20 Oct 2025 14:00:11 +1300 Subject: [PATCH 7/9] Fix tests --- .../azure-core/azure/core/rest/_aiohttp.py | 25 +++++++++++++++++-- .../async_tests/test_basic_transport_async.py | 17 +++++++------ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/sdk/core/azure-core/azure/core/rest/_aiohttp.py b/sdk/core/azure-core/azure/core/rest/_aiohttp.py index 5b7f6b0934a0..a93d6d8788a2 100644 --- a/sdk/core/azure-core/azure/core/rest/_aiohttp.py +++ b/sdk/core/azure-core/azure/core/rest/_aiohttp.py @@ -27,14 +27,23 @@ import asyncio # pylint: disable=do-not-import-asyncio from itertools import groupby from typing import Iterator, cast + +import aiohttp from multidict import CIMultiDict + from ._http_response_impl_async import ( AsyncHttpResponseImpl, AsyncHttpResponseBackcompatMixin, ) from ..pipeline.transport._aiohttp import AioHttpStreamDownloadGenerator from ..utils._pipeline_transport_rest_shared import _pad_attr_name, _aiohttp_body_helper -from ..exceptions import ResponseNotReadError +from ..exceptions import ( + ResponseNotReadError, + IncompleteReadError, + ServiceResponseError, + ServiceResponseTimeoutError, + ServiceRequestError +) class _ItemsView(collections.abc.ItemsView): @@ -211,7 +220,19 @@ async def read(self) -> bytes: :rtype: bytes """ if not self._content: - self._content = await super().read() + self._stream_download_check() + try: + self._content = await self._internal_response.read() + except aiohttp.client_exceptions.ClientPayloadError as err: + # This is the case that server closes connection before we finish the reading. aiohttp library + # raises ClientPayloadError. + raise IncompleteReadError(err, error=err) from err + except aiohttp.client_exceptions.ClientResponseError as err: + raise ServiceResponseError(err, error=err) from err + except asyncio.TimeoutError as err: + raise ServiceResponseTimeoutError(err, error=err) from err + except aiohttp.client_exceptions.ClientError as err: + raise ServiceRequestError(err, error=err) from err await self._set_read_checks() return _aiohttp_body_helper(self) diff --git a/sdk/core/azure-core/tests/async_tests/test_basic_transport_async.py b/sdk/core/azure-core/tests/async_tests/test_basic_transport_async.py index dfa0090ac450..63e20a2b0562 100644 --- a/sdk/core/azure-core/tests/async_tests/test_basic_transport_async.py +++ b/sdk/core/azure-core/tests/async_tests/test_basic_transport_async.py @@ -1060,15 +1060,18 @@ async def test_aiohttp_timeout_response(port, http_request): request = http_request("GET", f"http://localhost:{port}/basic/string") - with pytest.raises(ServiceResponseTimeoutError) as err: - await transport.send(request, read_timeout=0.0001) + with mock.patch.object( + aiohttp.ClientResponse, "start", side_effect=asyncio.TimeoutError("Too slow!") + ) as mock_method: + with pytest.raises(ServiceResponseTimeoutError) as err: + await transport.send(request) - with pytest.raises(ServiceResponseError) as err: - await transport.send(request, read_timeout=0.0001) + with pytest.raises(ServiceResponseError) as err: + await transport.send(request) - stream_resp = http_request("GET", f"http://localhost:{port}/streams/basic") - with pytest.raises(ServiceResponseTimeoutError) as err: - await transport.send(stream_resp, stream=True, read_timeout=0.0001) + stream_resp = http_request("GET", f"http://localhost:{port}/streams/basic") + with pytest.raises(ServiceResponseTimeoutError) as err: + await transport.send(stream_resp, stream=True) stream_resp = await transport.send(stream_resp, stream=True) with mock.patch.object( From 377235c157283b18784833bcb596a58d4eb0db71 Mon Sep 17 00:00:00 2001 From: antisch Date: Mon, 20 Oct 2025 14:26:57 +1300 Subject: [PATCH 8/9] Even blacker --- sdk/core/azure-core/azure/core/rest/_aiohttp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/core/azure-core/azure/core/rest/_aiohttp.py b/sdk/core/azure-core/azure/core/rest/_aiohttp.py index a93d6d8788a2..ebcae4844872 100644 --- a/sdk/core/azure-core/azure/core/rest/_aiohttp.py +++ b/sdk/core/azure-core/azure/core/rest/_aiohttp.py @@ -42,7 +42,7 @@ IncompleteReadError, ServiceResponseError, ServiceResponseTimeoutError, - ServiceRequestError + ServiceRequestError, ) From 26c67e09332c0e18dc1eb31c897b1e6cdd7ca1e8 Mon Sep 17 00:00:00 2001 From: antisch Date: Tue, 28 Oct 2025 08:49:27 +1300 Subject: [PATCH 9/9] Update CHANGELOG.md --- sdk/core/azure-core/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sdk/core/azure-core/CHANGELOG.md b/sdk/core/azure-core/CHANGELOG.md index 91a09d2943bf..4355e8d9d0f6 100644 --- a/sdk/core/azure-core/CHANGELOG.md +++ b/sdk/core/azure-core/CHANGELOG.md @@ -9,6 +9,8 @@ ### Bugs Fixed - Fixed repeated import attempts of cchardet and chardet when charset_normalizer is used #43092 +- Fixed leaked requests and aiohttp exceptions for streamed responses #43200 +- Improved granularity of ServiceRequestError and ServiceResponseError exceptions raised in timeout scenarios from the requests and aiohttp transports #43200 ### Other Changes