Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions test/test_realtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from gql.transport.exceptions import TransportConnectionFailed, TransportError
from websockets.asyncio.connection import State

import tibber
from tibber.exceptions import SubscriptionEndpointMissingError, WebsocketReconnectedError, WebsocketTransportError
from tibber.realtime import TibberRT, TibberWebsocketsTransport

Expand Down Expand Up @@ -205,6 +206,31 @@ async def test_reconnect_rebuilds_transport_when_token_unchanged(
await tibber_rt.disconnect()


async def test_reconnect_completes_when_refresh_token_callback_raises(
mock_client: MagicMock,
) -> None:
"""A raising refresh_access_token callback must not abort the reconnect flow."""
tibber_connection = tibber.Tibber(
access_token="test_token",
websession=MagicMock(),
user_agent="test_agent",
refresh_access_token=AsyncMock(side_effect=Exception),
)
await tibber_connection.realtime.set_subscription_endpoint("wss://test.endpoint")

await tibber_connection.realtime.connect()
first_transport = mock_client.transport
mock_client.transport.adapter.websocket = MagicMock(state=State.CLOSED)

await tibber_connection.realtime.reconnect()

mock_client.close_async.assert_awaited_once()
assert mock_client.transport is not first_transport
assert tibber_connection.realtime.subscription_running is True

await tibber_connection.realtime.disconnect()


async def test_transport_close_times_out_on_hanging_wait_closed(
monkeypatch: pytest.MonkeyPatch,
) -> None:
Expand Down
6 changes: 5 additions & 1 deletion tibber/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,11 @@ async def _refresh_access_token_for_reconnect(self) -> str | None:
if self._refresh_access_token is None:
return None

access_token = await self._refresh_access_token()
try:
access_token = await self._refresh_access_token()
except Exception:

@augmentcode augmentcode Bot Aug 17, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TibberRT is also an exported public class and accepts refresh_access_token directly, but its reconnect() still awaits that callback unguarded at tibber/realtime.py:154. Consequently, direct TibberRT users still have reconnect abort when their externally defined callback raises, despite the behavior this change intends to provide.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

@MartinHjelmare MartinHjelmare Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think TibberRT is expected to be called separately, but in any case I think that's a separate problem.

_LOGGER.exception("Error refreshing access token")
return None
if access_token is not None and access_token != self._access_token:
_LOGGER.debug("Updating access token")
self._access_token = access_token
Expand Down