Repository: Azure/azure-sdk-for-python

Issue:
Separate mapping of aiohttp.ConnectionErrors from asyncio.TimeoutError into ServiceRequestError and ServiceResponseError respectively
### Context: 
#### This issue is based on this code snippet - https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/core/azure-core/azure/core/pipeline/transport/_aiohttp.py#L347 
#### azure-core exposes two types of errors: `ServiceRequestError` and `ServiceResponseError`
#### `ServiceRequestError` are safe to be retried for all kind of operations (read and writes) - https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/core/corehttp/corehttp/exceptions.py#L157
#### `ServiceResponseError` are safe to be retried only for idempotent operations (reads) - https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/core/corehttp/corehttp/exceptions.py#L163
### Issue:
#### Currently azure-core maps all kinds of `asyncio.TimeoutError` into `ServiceResponseError`, which works fine in most of the cases except for when a client is using aiohttp for async io. 
#### In aiohttp world, `aiohttp.ConnectionTimeoutError` represents all kind of errors happening because of client not being able to connect to server. All of these errors are safe to be retried for all kind of operations. The issue is that some of these errors also extend from `asyncio.TimeoutError` (because of multiple inheritence in python). And since azure-core first catches the `asynio.TimeoutError` and maps to `ServiceResponseError`, we cannot retry them for all operations since its agains the contract provided by azure-core. 
 https://docs.aiohttp.org/en/v3.7.2/client_reference.html#connection-errors
#### Because of this incorrect mapping, azure-cosmos currently cannot rely solely on `ServiceResponseError` or `ServiceRequestError` from azure-core, and has to take a runtime dependency on aiohttp directly to handle these `ClientConnectionErrors`.


Discussion:
@kashifkhan @xiangyan99
Thank you for your feedback. Tagging and routing to the team member best able to assist.
As a follow up to this working item, once this is done, azure-cosmos's code can change and can remove the dependency on runtime import of aiohttp. Tracking it here - https://github.com/Azure/azure-sdk-for-python/issues/39449
@kashifkhan / @xiangyan99 any update on this?



Patch:
diff --git a/sdk/core/azure-core/CHANGELOG.md b/sdk/core/azure-core/CHANGELOG.md
index 235d3324deeb..7eb21962bf72 100644
--- a/sdk/core/azure-core/CHANGELOG.md
+++ b/sdk/core/azure-core/CHANGELOG.md
@@ -1,6 +1,6 @@
 # Release History
 
-## 1.34.1 (Unreleased)
+## 1.35.0 (Unreleased)
 
 ### Features Added
 
@@ -10,6 +10,9 @@
 
 ### Other Changes
 
+- A timeout error when using the `aiohttp` transport (the default for async SDKs) will now be raised as a `azure.core.exceptions.ServiceResponseTimeoutError`, a subtype of the previously raised `ServiceResponseError`.
+- When using with `aiohttp` 3.10 or later, a connection timeout error will now be raised as a `azure.core.exceptions.ServiceRequestTimeoutError`, which can be retried.
+
 ## 1.34.0 (2025-05-01)
 
 ### Features Added
diff --git a/sdk/core/azure-core/azure/core/_version.py b/sdk/core/azure-core/azure/core/_version.py
index a8b22f835544..19d3f470bb98 100644
--- a/sdk/core/azure-core/azure/core/_version.py
+++ b/sdk/core/azure-core/azure/core/_version.py
@@ -9,4 +9,4 @@
 # regenerated.
 # --------------------------------------------------------------------------
 
-VERSION = "1.34.1"
+VERSION = "1.35.0"
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 28b5c261a5d5..8ef4d9ad33bb 100644
--- a/sdk/core/azure-core/azure/core/pipeline/transport/_aiohttp.py
+++ b/sdk/core/azure-core/azure/core/pipeline/transport/_aiohttp.py
@@ -49,7 +49,9 @@
 from azure.core.configuration import ConnectionConfiguration
 from azure.core.exceptions import (
     ServiceRequestError,
+    ServiceRequestTimeoutError,
     ServiceResponseError,
+    ServiceResponseTimeoutError,
     IncompleteReadError,
 )
 from azure.core.pipeline import AsyncPipeline
@@ -76,6 +78,15 @@
 CONTENT_CHUNK_SIZE = 10 * 1024
 _LOGGER = logging.getLogger(__name__)
 
+try:
+    # ConnectionTimeoutError was only introduced in aiohttp 3.10 so we want to keep this
+    # backwards compatible. If client is using aiohttp <3.10, the behaviour will safely
+    # fall back to treating a TimeoutError as a ServiceResponseError (that wont be retried).
+    from aiohttp.client_exceptions import ConnectionTimeoutError
+except ImportError:
+
+    class ConnectionTimeoutError(Exception): ...  # type: ignore[no-redef]
+
 
 class AioHttpTransport(AsyncHttpTransport):
     """AioHttp HTTP sender implementation.
@@ -344,8 +355,10 @@ async def send(
             raise
         except aiohttp.client_exceptions.ClientResponseError as err:
             raise ServiceResponseError(err, error=err) from err
+        except ConnectionTimeoutError as err:
+            raise ServiceRequestTimeoutError(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
         return response


Test patch:
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 0cf43195ed67..691996e7e71b 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
@@ -14,10 +14,19 @@
 from azure.core.rest._http_response_impl_async import AsyncHttpResponseImpl as RestAsyncHttpResponse
 from azure.core.pipeline.policies import HeadersPolicy
 from azure.core.pipeline import AsyncPipeline
-from azure.core.exceptions import HttpResponseError, ServiceResponseError
+from azure.core.exceptions import (
+    HttpResponseError,
+    ServiceResponseError,
+    ServiceRequestError,
+    ServiceRequestTimeoutError,
+    ServiceResponseTimeoutError,
+)
 from utils import HTTP_REQUESTS, request_and_responses_product
 import pytest
 import sys
+import asyncio
+from unittest.mock import Mock
+from pkg_resources import parse_version
 import aiohttp
 
 
@@ -1038,3 +1047,49 @@ async def test_close_too_soon_works_fine(caplog, port, http_request):
     result = await transport.send(request)
 
     assert result  # No exception is good enough here
+
+
+@pytest.mark.skipif(
+    parse_version(aiohttp.__version__) >= parse_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 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")
+
+        with pytest.raises(ServiceResponseTimeoutError) 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(ServiceResponseTimeoutError) as err:
+            await transport.send(stream_request, stream=True)
+
+
+@pytest.mark.skipif(
+    parse_version(aiohttp.__version__) < parse_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!"))
+
+        request = http_request("GET", f"http://localhost:12345/basic/string")
+
+        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)