diff --git a/onvif/client.py b/onvif/client.py
index 176d0df..fb072c1 100644
--- a/onvif/client.py
+++ b/onvif/client.py
@@ -248,12 +248,12 @@ def __init__(
self.dt_diff = dt_diff
self.binding_name = binding_name
# Create soap client
- connector = TCPConnector(
+ self._connector = TCPConnector(
ssl=_NO_VERIFY_SSL_CONTEXT,
keepalive_timeout=KEEPALIVE_EXPIRY,
)
- session = ClientSession(
- connector=connector,
+ self._session = ClientSession(
+ connector=self._connector,
timeout=aiohttp.ClientTimeout(
total=_DEFAULT_TIMEOUT,
connect=_CONNECT_TIMEOUT,
@@ -262,12 +262,12 @@ def __init__(
)
self.transport = (
AsyncTransportProtocolErrorHandler(
- session=session,
+ session=self._session,
verify_ssl=False,
)
if no_cache
else AIOHTTPTransport(
- session=session,
+ session=self._session,
verify_ssl=False,
cache=SqliteCache(),
)
@@ -316,6 +316,8 @@ async def setup(self):
async def close(self):
"""Close the transport."""
await self.transport.aclose()
+ await self._session.close()
+ await self._connector.close()
@staticmethod
@safe_func
diff --git a/onvif/managers.py b/onvif/managers.py
index 8f14030..cc60add 100644
--- a/onvif/managers.py
+++ b/onvif/managers.py
@@ -62,7 +62,7 @@ def __init__(
@property
def closed(self) -> bool:
"""Return True if the manager is closed."""
- return not self._subscription or self._subscription.transport.client.is_closed
+ return not self._subscription or self._subscription.transport.session.closed
async def start(self) -> None:
"""Setup the manager."""
diff --git a/onvif/zeep_aiohttp.py b/onvif/zeep_aiohttp.py
index 23d3f2f..1ec0acf 100644
--- a/onvif/zeep_aiohttp.py
+++ b/onvif/zeep_aiohttp.py
@@ -11,7 +11,6 @@
from zeep.utils import get_version
from zeep.wsdl.utils import etree_to_string
-import aiohttp
import httpx
from aiohttp import ClientResponse, ClientSession
from requests import Response
@@ -160,15 +159,14 @@ async def _post(
proxy=self.proxy,
timeout=self._client_timeout,
)
- response.raise_for_status()
- # Read the content to log it
+ # Read the content to log it before checking status
content = await response.read()
_LOGGER.debug(
"HTTP Response from %s (status: %d):\n%s",
address,
response.status,
- content.decode("utf-8", errors="replace"),
+ content,
)
# Convert to httpx Response
@@ -176,8 +174,6 @@ async def _post(
except TimeoutError as exc:
raise TimeoutError(f"Request to {address} timed out") from exc
- except aiohttp.ClientError as exc:
- raise ConnectionError(f"Error connecting to {address}: {exc}") from exc
async def post_xml(
self, address: str, envelope: _Element, headers: dict[str, str]
@@ -239,15 +235,15 @@ async def _get(
proxy=self.proxy,
timeout=self._client_timeout,
)
- response.raise_for_status()
- # Read content
+ # Read content and log before checking status
content = await response.read()
_LOGGER.debug(
- "HTTP Response from %s (status: %d)",
+ "HTTP Response from %s (status: %d):\n%s",
address,
response.status,
+ content,
)
# Convert directly to requests.Response
@@ -255,8 +251,6 @@ async def _get(
except TimeoutError as exc:
raise TimeoutError(f"Request to {address} timed out") from exc
- except aiohttp.ClientError as exc:
- raise ConnectionError(f"Error connecting to {address}: {exc}") from exc
def _httpx_to_requests_response(self, response: httpx.Response) -> Response:
"""Convert an httpx.Response object to a requests.Response object"""
diff --git a/tests/test_zeep_transport.py b/tests/test_zeep_transport.py
index f01c97b..c40d235 100644
--- a/tests/test_zeep_transport.py
+++ b/tests/test_zeep_transport.py
@@ -36,7 +36,6 @@ async def test_post_returns_httpx_response():
mock_aiohttp_response.url = "http://example.com/service"
mock_aiohttp_response.charset = "utf-8"
mock_aiohttp_response.cookies = {}
- mock_aiohttp_response.raise_for_status = Mock()
mock_content = b"test"
mock_aiohttp_response.read = AsyncMock(return_value=mock_content)
@@ -201,7 +200,7 @@ async def test_connection_error_handling():
transport.session = mock_session
- with pytest.raises(ConnectionError, match="Error connecting to"):
+ with pytest.raises(aiohttp.ClientError, match="Connection failed"):
await transport.get("http://example.com/wsdl")
@@ -869,3 +868,46 @@ async def test_cookie_jar_type():
# Verify cookies are accessible in requests response
assert hasattr(requests_result.cookies, "__getitem__")
assert "test" in requests_result.cookies
+
+
+@pytest.mark.asyncio
+async def test_http_error_responses_no_exception():
+ """Test that HTTP error responses (401, 500, etc.) don't raise exceptions."""
+ mock_session = create_mock_session()
+ transport = AIOHTTPTransport(session=mock_session)
+
+ # Test 401 Unauthorized
+ mock_401_response = Mock(spec=aiohttp.ClientResponse)
+ mock_401_response.status = 401
+ mock_401_response.headers = {"Content-Type": "text/xml"}
+ mock_401_response.method = "POST"
+ mock_401_response.url = "http://example.com/service"
+ mock_401_response.charset = "utf-8"
+ mock_401_response.cookies = {}
+ mock_401_response.read = AsyncMock(return_value=b"Unauthorized")
+
+ mock_session = Mock(spec=aiohttp.ClientSession)
+ mock_session.post = AsyncMock(return_value=mock_401_response)
+ transport.session = mock_session
+
+ # Should not raise exception
+ result = await transport.post("http://example.com/service", "", {})
+ assert isinstance(result, httpx.Response)
+ assert result.status_code == 401
+ assert result.read() == b"Unauthorized"
+
+ # Test 500 Internal Server Error
+ mock_500_response = Mock(spec=aiohttp.ClientResponse)
+ mock_500_response.status = 500
+ mock_500_response.headers = {"Content-Type": "text/xml"}
+ mock_500_response.charset = "utf-8"
+ mock_500_response.cookies = {}
+ mock_500_response.read = AsyncMock(return_value=b"Server Error")
+
+ mock_session.get = AsyncMock(return_value=mock_500_response)
+
+ # Should not raise exception
+ result = await transport.get("http://example.com/wsdl")
+ assert isinstance(result, RequestsResponse)
+ assert result.status_code == 500
+ assert result.content == b"Server Error"