Skip to content

Commit 70db3d9

Browse files
authored
More explicit error message if transport is already closed (#35559)
* Clarify error message if transport is already closed * Add tests * Add async impl and tests * Slight clarification on async path * If close before opening, don't fail * Simplify the code a bit * Pylint * ChangeLog * Thread safety * Adapt to new black
1 parent edccdfa commit 70db3d9

File tree

5 files changed

+143
-22
lines changed

5 files changed

+143
-22
lines changed

sdk/core/azure-core/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
### Bugs Fixed
1212

13+
- Raise correct exception if transport is used while already closed #35559
14+
1315
### Other Changes
1416

1517
- HTTP tracing spans will now include an `error.type` attribute if an error status code is returned. #34619

sdk/core/azure-core/azure/core/pipeline/transport/_aiohttp.py

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ def __init__(
112112
raise ValueError("session_owner cannot be False if no session is provided")
113113
self.connection_config = ConnectionConfiguration(**kwargs)
114114
self._use_env_settings = kwargs.pop("use_env_settings", True)
115+
# See https://github.com/Azure/azure-sdk-for-python/issues/25640 to understand why we track this
116+
self._has_been_opened = False
115117

116118
async def __aenter__(self):
117119
await self.open()
@@ -126,26 +128,33 @@ async def __aexit__(
126128
await self.close()
127129

128130
async def open(self):
129-
"""Opens the connection."""
130-
if not self.session and self._session_owner:
131-
jar = aiohttp.DummyCookieJar()
132-
clientsession_kwargs = {
133-
"trust_env": self._use_env_settings,
134-
"cookie_jar": jar,
135-
"auto_decompress": False,
136-
}
137-
if self._loop is not None:
138-
clientsession_kwargs["loop"] = self._loop
139-
self.session = aiohttp.ClientSession(**clientsession_kwargs)
140-
# pyright has trouble to understand that self.session is not None, since we raised at worst in the init
141-
self.session = cast(aiohttp.ClientSession, self.session)
131+
if self._has_been_opened and not self.session:
132+
raise ValueError(
133+
"HTTP transport has already been closed. "
134+
"You may check if you're calling a function outside of the `async with` of your client creation, "
135+
"or if you called `await close()` on your client already."
136+
)
137+
if not self.session:
138+
if self._session_owner:
139+
jar = aiohttp.DummyCookieJar()
140+
clientsession_kwargs = {
141+
"trust_env": self._use_env_settings,
142+
"cookie_jar": jar,
143+
"auto_decompress": False,
144+
}
145+
if self._loop is not None:
146+
clientsession_kwargs["loop"] = self._loop
147+
self.session = aiohttp.ClientSession(**clientsession_kwargs)
148+
else:
149+
raise ValueError("session_owner cannot be False and no session is available")
150+
151+
self._has_been_opened = True
142152
await self.session.__aenter__()
143153

144154
async def close(self):
145155
"""Closes the connection."""
146156
if self._session_owner and self.session:
147157
await self.session.close()
148-
self._session_owner = False
149158
self.session = None
150159

151160
def _build_ssl_config(self, cert, verify):
@@ -324,6 +333,13 @@ async def send(
324333
)
325334
if not stream_response:
326335
await response.load_body()
336+
except AttributeError as err:
337+
if self.session is None:
338+
raise ValueError(
339+
"No session available for request. "
340+
"Please report this issue to https://github.com/Azure/azure-sdk-for-python/issues."
341+
) from err
342+
raise
327343
except aiohttp.client_exceptions.ClientResponseError as err:
328344
raise ServiceResponseError(err, error=err) from err
329345
except asyncio.TimeoutError as err:

sdk/core/azure-core/azure/core/pipeline/transport/_requests_basic.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
#
2525
# --------------------------------------------------------------------------
2626
import logging
27-
from typing import Iterator, Optional, Union, TypeVar, overload, cast, TYPE_CHECKING, MutableMapping
27+
from typing import Iterator, Optional, Union, TypeVar, overload, TYPE_CHECKING, MutableMapping
2828
from urllib3.util.retry import Retry
2929
from urllib3.exceptions import (
3030
DecodeError as CoreDecodeError,
@@ -250,6 +250,8 @@ def __init__(self, **kwargs) -> None:
250250
raise ValueError("session_owner cannot be False if no session is provided")
251251
self.connection_config = ConnectionConfiguration(**kwargs)
252252
self._use_env_settings = kwargs.pop("use_env_settings", True)
253+
# See https://github.com/Azure/azure-sdk-for-python/issues/25640 to understand why we track this
254+
self._has_been_opened = False
253255

254256
def __enter__(self) -> "RequestsTransport":
255257
self.open()
@@ -272,16 +274,23 @@ def _init_session(self, session: requests.Session) -> None:
272274
session.mount(p, adapter)
273275

274276
def open(self):
275-
if not self.session and self._session_owner:
276-
self.session = requests.Session()
277-
self._init_session(self.session)
278-
# pyright has trouble to understand that self.session is not None, since we raised at worst in the init
279-
self.session = cast(requests.Session, self.session)
277+
if self._has_been_opened and not self.session:
278+
raise ValueError(
279+
"HTTP transport has already been closed. "
280+
"You may check if you're calling a function outside of the `with` of your client creation, "
281+
"or if you called `close()` on your client already."
282+
)
283+
if not self.session:
284+
if self._session_owner:
285+
self.session = requests.Session()
286+
self._init_session(self.session)
287+
else:
288+
raise ValueError("session_owner cannot be False and no session is available")
289+
self._has_been_opened = True
280290

281291
def close(self):
282292
if self._session_owner and self.session:
283293
self.session.close()
284-
self._session_owner = False
285294
self.session = None
286295

287296
@overload
@@ -312,7 +321,7 @@ def send(
312321
:keyword MutableMapping proxies: will define the proxy to use. Proxy is a dict (protocol, url)
313322
"""
314323

315-
def send(
324+
def send( # pylint: disable=too-many-statements
316325
self,
317326
request: Union[HttpRequest, "RestHttpRequest"],
318327
*,
@@ -358,6 +367,13 @@ def send(
358367
)
359368
response.raw.enforce_content_length = True
360369

370+
except AttributeError as err:
371+
if self.session is None:
372+
raise ValueError(
373+
"No session available for request. "
374+
"Please report this issue to https://github.com/Azure/azure-sdk-for-python/issues."
375+
) from err
376+
raise
361377
except (
362378
NewConnectionError,
363379
ConnectTimeoutError,

sdk/core/azure-core/tests/async_tests/test_basic_transport_async.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -993,3 +993,48 @@ async def test_aiohttp_errors():
993993
generator = AioHttpStreamDownloadGenerator(None, response)
994994
with pytest.raises(ServiceResponseError):
995995
await generator.__anext__()
996+
997+
998+
@pytest.mark.parametrize("http_request", HTTP_REQUESTS)
999+
@pytest.mark.asyncio
1000+
async def test_already_close_with_with(caplog, port, http_request):
1001+
transport = AioHttpTransport()
1002+
1003+
request = http_request("GET", "http://localhost:{}/basic/string".format(port))
1004+
1005+
async with AsyncPipeline(transport) as pipeline:
1006+
await pipeline.run(request)
1007+
1008+
# This is now closed, new requests should fail
1009+
with pytest.raises(ValueError) as err:
1010+
await transport.send(request)
1011+
assert "HTTP transport has already been closed." in str(err)
1012+
1013+
1014+
@pytest.mark.parametrize("http_request", HTTP_REQUESTS)
1015+
@pytest.mark.asyncio
1016+
async def test_already_close_manually(caplog, port, http_request):
1017+
transport = AioHttpTransport()
1018+
1019+
request = http_request("GET", "http://localhost:{}/basic/string".format(port))
1020+
1021+
await transport.send(request)
1022+
await transport.close()
1023+
1024+
# This is now closed, new requests should fail
1025+
with pytest.raises(ValueError) as err:
1026+
await transport.send(request)
1027+
assert "HTTP transport has already been closed." in str(err)
1028+
1029+
1030+
@pytest.mark.parametrize("http_request", HTTP_REQUESTS)
1031+
@pytest.mark.asyncio
1032+
async def test_close_too_soon_works_fine(caplog, port, http_request):
1033+
transport = AioHttpTransport()
1034+
1035+
request = http_request("GET", "http://localhost:{}/basic/string".format(port))
1036+
1037+
await transport.close()
1038+
result = await transport.send(request)
1039+
1040+
assert result # No exception is good enough here

sdk/core/azure-core/tests/test_basic_transport.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1280,3 +1280,45 @@ def test_conflict_timeout(caplog, port, http_request):
12801280
with pytest.raises(ValueError):
12811281
with Pipeline(transport) as pipeline:
12821282
pipeline.run(request, connection_timeout=(100, 100), read_timeout=100)
1283+
1284+
1285+
@pytest.mark.parametrize("http_request", HTTP_REQUESTS)
1286+
def test_already_close_with_with(caplog, port, http_request):
1287+
transport = RequestsTransport()
1288+
1289+
request = http_request("GET", "http://localhost:{}/basic/string".format(port))
1290+
1291+
with Pipeline(transport) as pipeline:
1292+
pipeline.run(request)
1293+
1294+
# This is now closed, new requests should fail
1295+
with pytest.raises(ValueError) as err:
1296+
transport.send(request)
1297+
assert "HTTP transport has already been closed." in str(err)
1298+
1299+
1300+
@pytest.mark.parametrize("http_request", HTTP_REQUESTS)
1301+
def test_already_close_manually(caplog, port, http_request):
1302+
transport = RequestsTransport()
1303+
1304+
request = http_request("GET", "http://localhost:{}/basic/string".format(port))
1305+
1306+
transport.send(request)
1307+
transport.close()
1308+
1309+
# This is now closed, new requests should fail
1310+
with pytest.raises(ValueError) as err:
1311+
transport.send(request)
1312+
assert "HTTP transport has already been closed." in str(err)
1313+
1314+
1315+
@pytest.mark.parametrize("http_request", HTTP_REQUESTS)
1316+
def test_close_too_soon_works_fine(caplog, port, http_request):
1317+
transport = RequestsTransport()
1318+
1319+
request = http_request("GET", "http://localhost:{}/basic/string".format(port))
1320+
1321+
transport.close() # Never opened, should work fine
1322+
result = transport.send(request)
1323+
1324+
assert result # No exception is good enough here

0 commit comments

Comments
 (0)