From 5cd90947fd3c0c91e48f9a9441ca16e571c59db2 Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Wed, 13 May 2026 23:05:30 -0700 Subject: [PATCH 1/2] fix: log RST_STREAM/error-code-0 at DEBUG instead of WARNING MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HTTP/2 RST_STREAM with error code 0 (NO_ERROR) is a normal server-side graceful reset — e.g. a load balancer recycling the connection — not a real error. Logging it at WARNING pollutes HA logs with spurious noise on every periodic reconnect. Detect this case via the error details string and downgrade to DEBUG. All other retryable gRPC errors continue to log at WARNING. --- CHANGELOG.md | 3 +++ src/quilt_hp/services/streaming.py | 10 ++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb3a555..14db8b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## [Unreleased] +### Fixed +- `RST_STREAM with error code 0` (HTTP/2 `NO_ERROR`, a normal server-side graceful reset) is now logged at `DEBUG` instead of `WARNING` to reduce log noise + ## [0.3.0] - 2026-05-12 ### Added diff --git a/src/quilt_hp/services/streaming.py b/src/quilt_hp/services/streaming.py index d4541e4..473b44f 100644 --- a/src/quilt_hp/services/streaming.py +++ b/src/quilt_hp/services/streaming.py @@ -630,10 +630,16 @@ async def _run_stream_with_reconnect(self) -> None: break elif can_retry: self._stream_state = "reconnecting" - logger.warning( + details = exc.details() or "" + # RST_STREAM/error-code-0 is HTTP/2 NO_ERROR — a normal + # server-side graceful reset (e.g. load-balancer recycling + # the connection). Log at DEBUG to avoid log noise. + is_graceful_reset = "RST_STREAM with error code 0" in details + log = logger.debug if is_graceful_reset else logger.warning + log( "Stream error %s: %s; reconnecting in %.1fs (attempt %d)", exc.code(), - exc.details(), + details, delay, attempt + 1, ) From f41fe45f675d3db19875b3662a7f429b4c2b3e44 Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Thu, 14 May 2026 08:23:42 -0700 Subject: [PATCH 2/2] fix: downgrade CANCELLED and UNAUTHENTICATED reconnect logs from WARNING to INFO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CANCELLED (server closed the stream normally, e.g. keepalive timeout or server-side rotation) and UNAUTHENTICATED (handled automatically by the token-refresh callback) are both expected, self-healing events that should not appear as warnings in the HA log. Log levels for stream reconnection events: DEBUG — RST_STREAM with error code 0 (HTTP/2 NO_ERROR graceful reset) INFO — CANCELLED (server-side stream lifecycle) INFO — UNAUTHENTICATED (auto token refresh) WARNING — any other unexpected error code --- CHANGELOG.md | 2 ++ src/quilt_hp/services/streaming.py | 21 ++++++++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14db8b8..bfbadfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Fixed - `RST_STREAM with error code 0` (HTTP/2 `NO_ERROR`, a normal server-side graceful reset) is now logged at `DEBUG` instead of `WARNING` to reduce log noise +- `CANCELLED` (server closed the stream normally, e.g. keepalive timeout or server rotation) is now logged at `INFO` instead of `WARNING` +- `UNAUTHENTICATED` reconnects handled by the automatic token refresh are now logged at `INFO` instead of `WARNING` ## [0.3.0] - 2026-05-12 diff --git a/src/quilt_hp/services/streaming.py b/src/quilt_hp/services/streaming.py index 473b44f..7506309 100644 --- a/src/quilt_hp/services/streaming.py +++ b/src/quilt_hp/services/streaming.py @@ -612,7 +612,9 @@ async def _run_stream_with_reconnect(self) -> None: if is_unauth and self._authenticate is not None and can_retry: self._stream_state = "reconnecting" - logger.warning( + # Token expiry is handled automatically — INFO to confirm it + # happened without alarming the user. + logger.info( "Stream got UNAUTHENTICATED; refreshing token (attempt %d)", attempt + 1, ) @@ -631,11 +633,20 @@ async def _run_stream_with_reconnect(self) -> None: elif can_retry: self._stream_state = "reconnecting" details = exc.details() or "" - # RST_STREAM/error-code-0 is HTTP/2 NO_ERROR — a normal - # server-side graceful reset (e.g. load-balancer recycling - # the connection). Log at DEBUG to avoid log noise. + # Classify the error to pick the right log level: + # DEBUG — HTTP/2 NO_ERROR RST_STREAM: server gracefully + # recycled the connection (load balancer, keepalive). + # INFO — CANCELLED: server closed the stream normally + # (keepalive timeout, server rotation, etc.). + # WARNING — anything else is unexpected. is_graceful_reset = "RST_STREAM with error code 0" in details - log = logger.debug if is_graceful_reset else logger.warning + is_server_cancel = exc.code() == grpc.StatusCode.CANCELLED + if is_graceful_reset: + log = logger.debug + elif is_server_cancel: + log = logger.info + else: + log = logger.warning log( "Stream error %s: %s; reconnecting in %.1fs (attempt %d)", exc.code(),