Skip to content

Commit 64c9311

Browse files
authored
Handle cameras that close connections without Connection: close header (#140)
1 parent 5ce2fec commit 64c9311

File tree

3 files changed

+590
-4
lines changed

3 files changed

+590
-4
lines changed

onvif/client.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,17 @@ class AsyncTransportProtocolErrorHandler(AIOHTTPTransport):
114114
# once since
115115
"""
116116

117-
@retry_connection_error(attempts=2, exception=aiohttp.ServerDisconnectedError)
117+
@retry_connection_error(
118+
attempts=2, exception=aiohttp.ServerDisconnectedError, backoff=0
119+
)
118120
async def post(
119121
self, address: str, message: str, headers: dict[str, str]
120122
) -> httpx.Response:
121123
return await super().post(address, message, headers)
122124

123-
@retry_connection_error(attempts=2, exception=aiohttp.ServerDisconnectedError)
125+
@retry_connection_error(
126+
attempts=2, exception=aiohttp.ServerDisconnectedError, backoff=0
127+
)
124128
async def get(
125129
self,
126130
address: str,
@@ -129,6 +133,14 @@ async def get(
129133
) -> Response:
130134
return await super().get(address, params, headers)
131135

136+
@retry_connection_error(
137+
attempts=2, exception=aiohttp.ServerDisconnectedError, backoff=0
138+
)
139+
async def post_xml(
140+
self, address: str, envelope: Any, headers: dict[str, str]
141+
) -> Response:
142+
return await super().post_xml(address, envelope, headers)
143+
132144

133145
async def _cached_document(url: str) -> Document:
134146
"""Load external XML document from disk."""

onvif/wrappers.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@
1919
def retry_connection_error(
2020
attempts: int = DEFAULT_ATTEMPTS,
2121
exception: type[Exception] = aiohttp.ClientError,
22+
backoff: float | None = None,
2223
) -> Callable[[Callable[P, Awaitable[T]]], Callable[P, Awaitable[T]]]:
2324
"""Define a wrapper to retry on connection error."""
25+
if backoff is None:
26+
backoff = BACKOFF_TIME
2427

2528
def _decorator_retry_connection_error(
2629
func: Callable[P, Awaitable[T]],
@@ -35,8 +38,17 @@ def _decorator_retry_connection_error(
3538
async def _async_wrap_connection_error_retry( # type: ignore[return]
3639
*args: P.args, **kwargs: P.kwargs
3740
) -> T:
41+
logger.debug(
42+
"retry_connection_error wrapper called for %s with exception=%s, attempts=%s",
43+
func.__name__,
44+
exception,
45+
attempts,
46+
)
3847
for attempt in range(attempts):
3948
try:
49+
logger.debug(
50+
"Attempt %s/%s for %s", attempt + 1, attempts, func.__name__
51+
)
4052
return await func(*args, **kwargs)
4153
except exception as ex:
4254
#
@@ -49,16 +61,25 @@ async def _async_wrap_connection_error_retry( # type: ignore[return]
4961
# to close the connection at any time, we treat this as a normal and try again
5062
# once since we do not want to declare the camera as not supporting PullPoint
5163
# if it just happened to close the connection at the wrong time.
64+
logger.debug(
65+
"Caught exception %s (type: %s) on attempt %s/%s",
66+
ex,
67+
type(ex).__name__,
68+
attempt + 1,
69+
attempts,
70+
exc_info=True,
71+
)
5272
if attempt == attempts - 1:
73+
logger.debug("Final attempt failed, re-raising exception")
5374
raise
5475
logger.debug(
5576
"Error: %s while calling %s, backing off: %s, retrying...",
5677
ex,
5778
func,
58-
BACKOFF_TIME,
79+
backoff,
5980
exc_info=True,
6081
)
61-
await asyncio.sleep(BACKOFF_TIME)
82+
await asyncio.sleep(backoff)
6283

6384
return _async_wrap_connection_error_retry
6485

0 commit comments

Comments
 (0)