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"