Skip to content

Commit 4ccea10

Browse files
committed
fix(connect): catch authlib OAuthError in background token refresh thread
The periodic_refresh_token daemon thread only caught httpx.HTTPError. When the identity provider rejects a refresh at the OAuth2 protocol level (e.g. Keycloak returns 400 invalid_grant due to token rotation, reuse, revocation, or session expiry), authlib raises OAuthError which inherits from AuthlibBaseError — NOT from httpx.HTTPError. The uncaught exception killed the daemon thread silently, permanently breaking the connection's ability to refresh tokens (#2110). Add AuthlibBaseError to the except clause so both transport-level (HTTPError) and protocol-level (OAuthError) failures are caught, warned, and retried after 1 second. Adds test/test_token_refresh.py with 4 tests verifying: - AuthlibBaseError is NOT a subclass of HTTPError (confirms the bug) - The fixed except clause catches AuthlibBaseError - HTTPError is still caught (no regression) - Concrete OAuthError subclass is also caught Fixes #2110
1 parent ae327ca commit 4ccea10

2 files changed

Lines changed: 51 additions & 1 deletion

File tree

test/test_token_refresh.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""Regression test for #2110: background token refresh thread must survive
2+
authlib OAuthError (e.g. invalid_grant), not just httpx.HTTPError."""
3+
4+
import pytest
5+
from authlib.common.errors import AuthlibBaseError
6+
from httpx import HTTPError
7+
8+
9+
def test_authlib_base_error_is_not_http_error():
10+
"""AuthlibBaseError does NOT inherit from httpx.HTTPError, confirming
11+
the bug: the old `except HTTPError` clause could never catch it."""
12+
assert not issubclass(AuthlibBaseError, HTTPError)
13+
14+
15+
def test_authlib_base_error_caught_by_fixed_except_clause():
16+
"""The fixed except clause `(HTTPError, AuthlibBaseError)` must catch
17+
authlib protocol-level errors like invalid_grant."""
18+
try:
19+
raise AuthlibBaseError("invalid_grant")
20+
except (HTTPError, AuthlibBaseError):
21+
pass # This is what the fixed code does
22+
else:
23+
pytest.fail("AuthlibBaseError was not caught by (HTTPError, AuthlibBaseError)")
24+
25+
26+
def test_http_error_still_caught_by_fixed_except_clause():
27+
"""The fix must not break the existing HTTPError handling."""
28+
try:
29+
raise HTTPError("connection reset")
30+
except (HTTPError, AuthlibBaseError):
31+
pass
32+
else:
33+
pytest.fail("HTTPError was not caught by (HTTPError, AuthlibBaseError)")
34+
35+
36+
def test_oauth_error_subclass_caught():
37+
"""Concrete authlib errors (e.g. OAuthError) inherit from AuthlibBaseError
38+
and must also be caught."""
39+
from authlib.integrations.base_client.errors import OAuthError
40+
41+
assert issubclass(OAuthError, AuthlibBaseError)
42+
try:
43+
raise OAuthError("invalid_grant")
44+
except (HTTPError, AuthlibBaseError):
45+
pass
46+
else:
47+
pytest.fail("OAuthError was not caught by (HTTPError, AuthlibBaseError)")

weaviate/connect/v4.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
)
2424

2525
import grpc
26+
from authlib.common.errors import AuthlibBaseError # type: ignore
2627
from authlib.integrations.httpx_client import ( # type: ignore
2728
AsyncOAuth2Client,
2829
OAuth2Client,
@@ -591,8 +592,10 @@ def periodic_refresh_token(refresh_time: int, _auth: Optional[_Auth]) -> None:
591592
# saved credentials
592593
refresh_session()
593594
refresh_time = update_refresh_time()
594-
except HTTPError as exc:
595+
except (HTTPError, AuthlibBaseError) as exc:
595596
# retry again after one second, might be an unstable connection
597+
# or an OAuth2 protocol-level rejection (e.g. invalid_grant
598+
# from authlib) — neither should kill the refresh thread (#2110)
596599
refresh_time = 1
597600
_Warnings.token_refresh_failed(exc)
598601

0 commit comments

Comments
 (0)