From 4ccea10f3570ae74a65ccce1f74f2345eee3e806 Mon Sep 17 00:00:00 2001 From: Solaris-star <820622658@qq.com> Date: Thu, 30 Jul 2026 19:14:17 +0800 Subject: [PATCH] fix(connect): catch authlib OAuthError in background token refresh thread MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- test/test_token_refresh.py | 47 ++++++++++++++++++++++++++++++++++++++ weaviate/connect/v4.py | 5 +++- 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 test/test_token_refresh.py diff --git a/test/test_token_refresh.py b/test/test_token_refresh.py new file mode 100644 index 000000000..bcee5d194 --- /dev/null +++ b/test/test_token_refresh.py @@ -0,0 +1,47 @@ +"""Regression test for #2110: background token refresh thread must survive +authlib OAuthError (e.g. invalid_grant), not just httpx.HTTPError.""" + +import pytest +from authlib.common.errors import AuthlibBaseError +from httpx import HTTPError + + +def test_authlib_base_error_is_not_http_error(): + """AuthlibBaseError does NOT inherit from httpx.HTTPError, confirming + the bug: the old `except HTTPError` clause could never catch it.""" + assert not issubclass(AuthlibBaseError, HTTPError) + + +def test_authlib_base_error_caught_by_fixed_except_clause(): + """The fixed except clause `(HTTPError, AuthlibBaseError)` must catch + authlib protocol-level errors like invalid_grant.""" + try: + raise AuthlibBaseError("invalid_grant") + except (HTTPError, AuthlibBaseError): + pass # This is what the fixed code does + else: + pytest.fail("AuthlibBaseError was not caught by (HTTPError, AuthlibBaseError)") + + +def test_http_error_still_caught_by_fixed_except_clause(): + """The fix must not break the existing HTTPError handling.""" + try: + raise HTTPError("connection reset") + except (HTTPError, AuthlibBaseError): + pass + else: + pytest.fail("HTTPError was not caught by (HTTPError, AuthlibBaseError)") + + +def test_oauth_error_subclass_caught(): + """Concrete authlib errors (e.g. OAuthError) inherit from AuthlibBaseError + and must also be caught.""" + from authlib.integrations.base_client.errors import OAuthError + + assert issubclass(OAuthError, AuthlibBaseError) + try: + raise OAuthError("invalid_grant") + except (HTTPError, AuthlibBaseError): + pass + else: + pytest.fail("OAuthError was not caught by (HTTPError, AuthlibBaseError)") diff --git a/weaviate/connect/v4.py b/weaviate/connect/v4.py index d70bfc77b..c887baaa7 100644 --- a/weaviate/connect/v4.py +++ b/weaviate/connect/v4.py @@ -23,6 +23,7 @@ ) import grpc +from authlib.common.errors import AuthlibBaseError # type: ignore from authlib.integrations.httpx_client import ( # type: ignore AsyncOAuth2Client, OAuth2Client, @@ -591,8 +592,10 @@ def periodic_refresh_token(refresh_time: int, _auth: Optional[_Auth]) -> None: # saved credentials refresh_session() refresh_time = update_refresh_time() - except HTTPError as exc: + except (HTTPError, AuthlibBaseError) as exc: # retry again after one second, might be an unstable connection + # or an OAuth2 protocol-level rejection (e.g. invalid_grant + # from authlib) — neither should kill the refresh thread (#2110) refresh_time = 1 _Warnings.token_refresh_failed(exc)