From e6eddf5bc38b27365b6702c5f55c5ea6857accb8 Mon Sep 17 00:00:00 2001 From: disqualifier Date: Sun, 9 Aug 2026 02:08:58 -0400 Subject: [PATCH] fix: logging discipline in request()/request_with_retries - delete the per-attempt log.error before raise in request()'s ClientError branch: the path re-raises (raise XOR log), commons.aretry already emits the per-attempt WARNING with the attempt count, and the terminal branch logs on exhaustion - the line was pure duplicate reporting of a retried failure. - demote the retryable-status per-attempt line to debug for the same reason (it raises _RetryStatus; aretry owns the retry WARNING). - level the four terminal branches of request_with_retries from ERROR to WARNING: they SWALLOW exhaustion into a falsy FailureResponse the caller branches on (recovered cleanly per the swallow contract), so the lib does not claim ERROR on the caller's behalf; the caller escalates on the falsy result. all four stay one uniform level. verified: the terminal branch still fires on genuine exhaustion, request_with_retries still returns a falsy FailureResponse, and the deleted per-attempt ERROR no longer emits. Signed-off-by: disqualifier --- src/aioweb/session.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/aioweb/session.py b/src/aioweb/session.py index a503696..c7bfd77 100644 --- a/src/aioweb/session.py +++ b/src/aioweb/session.py @@ -470,10 +470,12 @@ class ExtendedSession: # get a typed failure and request_with_retries can label it a timeout. now also # catches the hang-guard firing (asyncio.wait_for raises asyncio.TimeoutError). raise aiohttp.ServerTimeoutError(f"timeout for {url}: {error}") from error - except aiohttp.ClientError as error: + except aiohttp.ClientError: # re-raise the original subtype (not flattened) - request_with_retries still - # catches the base aiohttp.ClientError below and is unaffected - log.error("client error for %s: %s", url, error) + # catches the base aiohttp.ClientError below and is unaffected. no log here: + # this path RAISES, so the exception carries the failure (raise XOR log); on the + # retrying path commons.aretry emits the per-attempt WARNING and the terminal + # branch logs on exhaustion - logging here would double-report the same failure. raise async def request_with_retries( @@ -507,7 +509,11 @@ class ExtendedSession: headers=headers, proxies=proxies, timeout=timeout, debug=debug, ) if response.status_code in retry_statuses: - log.warning("retryable status %s for %s", response.status_code, url) + # debug, not warning: this RAISES _RetryStatus to trigger a retry, so it must + # not also warn/error the same failure (raise XOR log). commons.aretry emits + # the per-attempt WARNING with the attempt count when it catches and retries; + # the terminal branch below logs on exhaustion. + log.debug("retryable status %s for %s", response.status_code, url) raise _RetryStatus(response) return response @@ -516,19 +522,25 @@ class ExtendedSession: attempt, attempts=attempts, backoff=1.0, factor=backoff_base, jitter=False, on=(Exception,), ) + # terminal path: retries are exhausted and the failure is SWALLOWED into a falsy + # FailureResponse the caller branches on (the documented contract). rule 2 mandates a + # log on a swallow; level is WARNING because the lib recovered cleanly into a + # branchable value - the failing op is the caller's to escalate once it sees the falsy + # result, so the lib does not claim ERROR on the caller's behalf. all four branches are + # the same event class (exhausted retries) and stay at one uniform level. except _RetryStatus as exhausted: - log.error("all %d attempts failed for %s (last status %s)", - attempts, url, exhausted.response.status_code) + log.warning("all %d attempts failed for %s (last status %s)", + attempts, url, exhausted.response.status_code) return exhausted.response except asyncio.TimeoutError: # catch before ClientError so a timeout is labeled as such, not generic - log.error("all %d attempts timed out for %s", attempts, url) + log.warning("all %d attempts timed out for %s", attempts, url) return FailureResponse(reason="timeout", url=url) except aiohttp.ClientError as error: - log.error("all %d attempts failed for %s (client error: %s)", attempts, url, error) + log.warning("all %d attempts failed for %s (client error: %s)", attempts, url, error) return FailureResponse(reason=f"client error: {error}", url=url) except Exception as error: - log.error("all %d attempts failed for %s (unexpected: %s)", attempts, url, error) + log.warning("all %d attempts failed for %s (unexpected: %s)", attempts, url, error) return FailureResponse(reason=f"unexpected error: {error}", url=url) # -------------------------------------------------------------------------