dabla opened a new issue, #59359:
URL: https://github.com/apache/airflow/issues/59359

   ### Apache Airflow Provider(s)
   
   keycloak
   
   ### Versions of Apache Airflow Providers
   
   We are using the latest airflow providers shipped with Airflow 3.1.4 
constraints.
   
   The issue arised with Airflow 3.1.4 and apache-airflow-providers-keycloak 
0.3.0.
   
   ### Apache Airflow version
   
   3.1.4
   
   ### Operating System
   
   Linux
   
   ### Deployment
   
   Other 3rd-party Helm chart
   
   ### Deployment details
   
   _No response_
   
   ### What happened
   
   When we keep a browser open for a long time, like overnight, and we want to 
refresh the Airflow dags page, the Airflow api server crashes with following 
exception:
   
   ```
   INFO:     172.31.52.95:0 - "GET /favicon.ico HTTP/1.1" 500 Internal Server 
Error
   ERROR:    Exception in ASGI application
     + Exception Group Traceback (most recent call last):
     |   File "/usr/local/lib/python3.13/site-packages/starlette/_utils.py", 
line 79, in collapse_excgroups
     |     yield
     |   File 
"/usr/local/lib/python3.13/site-packages/starlette/middleware/base.py", line 
183, in __call__
     |     async with anyio.create_task_group() as task_group:
     |                ~~~~~~~~~~~~~~~~~~~~~~~^^
     |   File 
"/usr/local/lib/python3.13/site-packages/anyio/_backends/_asyncio.py", line 
783, in __aexit__
     |     raise BaseExceptionGroup(
     |         "unhandled errors in a TaskGroup", self._exceptions
     |     ) from None
     | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
     +-+---------------- 1 ----------------
       | Traceback (most recent call last):
       |   File 
"/usr/local/lib/python3.13/site-packages/uvicorn/protocols/http/httptools_impl.py",
 line 409, in run_asgi
       |     result = await app(  # type: ignore[func-returns-value]
       |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       |         self.scope, self.receive, self.send
       |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       |     )
       |     ^
       |   File 
"/usr/local/lib/python3.13/site-packages/uvicorn/middleware/proxy_headers.py", 
line 60, in __call__
       |     return await self.app(scope, receive, send)
       |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       |   File 
"/usr/local/lib/python3.13/site-packages/fastapi/applications.py", line 1082, 
in __call__
       |     await super().__call__(scope, receive, send)
       |   File 
"/usr/local/lib/python3.13/site-packages/starlette/applications.py", line 113, 
in __call__
       |     await self.middleware_stack(scope, receive, send)
       |   File 
"/usr/local/lib/python3.13/site-packages/starlette/middleware/errors.py", line 
186, in __call__
       |     raise exc
       |   File 
"/usr/local/lib/python3.13/site-packages/starlette/middleware/errors.py", line 
164, in __call__
       |     await self.app(scope, receive, _send)
       |   File 
"/usr/local/lib/python3.13/site-packages/starlette/middleware/gzip.py", line 
29, in __call__
       |     await responder(scope, receive, send)
       |   File 
"/usr/local/lib/python3.13/site-packages/starlette/middleware/gzip.py", line 
130, in __call__
       |     await super().__call__(scope, receive, send)
       |   File 
"/usr/local/lib/python3.13/site-packages/starlette/middleware/gzip.py", line 
46, in __call__
       |     await self.app(scope, receive, self.send_with_compression)
       |   File 
"/usr/local/lib/python3.13/site-packages/starlette/middleware/cors.py", line 
85, in __call__
       |     await self.app(scope, receive, send)
       |   File 
"/usr/local/lib/python3.13/site-packages/starlette/middleware/base.py", line 
182, in __call__
       |     with recv_stream, send_stream, collapse_excgroups():
       |                                    ~~~~~~~~~~~~~~~~~~^^
       |   File "/usr/lib64/python3.13/contextlib.py", line 162, in __exit__
       |     self.gen.throw(value)
       |     ~~~~~~~~~~~~~~^^^^^^^
       |   File "/usr/local/lib/python3.13/site-packages/starlette/_utils.py", 
line 85, in collapse_excgroups
       |     raise exc
       |   File 
"/usr/local/lib/python3.13/site-packages/starlette/middleware/base.py", line 
184, in __call__
       |     response = await self.dispatch_func(request, call_next)
       |                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       |   File 
"/usr/local/lib/python3.13/site-packages/airflow/api_fastapi/auth/middlewares/refresh_token.py",
 line 45, in dispatch
       |     new_user = await self._refresh_user(current_token)
       |                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       |   File 
"/usr/local/lib/python3.13/site-packages/airflow/api_fastapi/auth/middlewares/refresh_token.py",
 line 68, in _refresh_user
       |     return get_auth_manager().refresh_user(user=user)
       |            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
       |   File 
"/usr/local/lib/python3.13/site-packages/airflow/providers/keycloak/auth_manager/keycloak_auth_manager.py",
 line 121, in refresh_user
       |     tokens = client.refresh_token(user.refresh_token)
       |   File 
"/usr/local/lib/python3.13/site-packages/keycloak/keycloak_openid.py", line 
410, in refresh_token
       |     return raise_error_from_response(data_raw, KeycloakPostError)
       |   File 
"/usr/local/lib/python3.13/site-packages/keycloak/exceptions.py", line 195, in 
raise_error_from_response
       |     raise error(
       |     ...<3 lines>...
       |     )
       | keycloak.exceptions.KeycloakPostError: 400: 
b'{"error":"invalid_grant","error_description":"Token is not active"}'
       +------------------------------------
   
   During handling of the above exception, another exception occurred:
   
   Traceback (most recent call last):
     File 
"/usr/local/lib/python3.13/site-packages/uvicorn/protocols/http/httptools_impl.py",
 line 409, in run_asgi
       result = await app(  # type: ignore[func-returns-value]
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
           self.scope, self.receive, self.send
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       )
       ^
     File 
"/usr/local/lib/python3.13/site-packages/uvicorn/middleware/proxy_headers.py", 
line 60, in __call__
       return await self.app(scope, receive, send)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     File "/usr/local/lib/python3.13/site-packages/fastapi/applications.py", 
line 1082, in __call__
       await super().__call__(scope, receive, send)
     File "/usr/local/lib/python3.13/site-packages/starlette/applications.py", 
line 113, in __call__
       await self.middleware_stack(scope, receive, send)
     File 
"/usr/local/lib/python3.13/site-packages/starlette/middleware/errors.py", line 
186, in __call__
       raise exc
     File 
"/usr/local/lib/python3.13/site-packages/starlette/middleware/errors.py", line 
164, in __call__
       await self.app(scope, receive, _send)
     File 
"/usr/local/lib/python3.13/site-packages/starlette/middleware/gzip.py", line 
29, in __call__
       await responder(scope, receive, send)
     File 
"/usr/local/lib/python3.13/site-packages/starlette/middleware/gzip.py", line 
130, in __call__
       await super().__call__(scope, receive, send)
     File 
"/usr/local/lib/python3.13/site-packages/starlette/middleware/gzip.py", line 
46, in __call__
       await self.app(scope, receive, self.send_with_compression)
     File 
"/usr/local/lib/python3.13/site-packages/starlette/middleware/cors.py", line 
85, in __call__
       await self.app(scope, receive, send)
     File 
"/usr/local/lib/python3.13/site-packages/starlette/middleware/base.py", line 
182, in __call__
       with recv_stream, send_stream, collapse_excgroups():
                                      ~~~~~~~~~~~~~~~~~~^^
     File "/usr/lib64/python3.13/contextlib.py", line 162, in __exit__
       self.gen.throw(value)
       ~~~~~~~~~~~~~~^^^^^^^
     File "/usr/local/lib/python3.13/site-packages/starlette/_utils.py", line 
85, in collapse_excgroups
       raise exc
     File 
"/usr/local/lib/python3.13/site-packages/starlette/middleware/base.py", line 
184, in __call__
       response = await self.dispatch_func(request, call_next)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     File 
"/usr/local/lib/python3.13/site-packages/airflow/api_fastapi/auth/middlewares/refresh_token.py",
 line 45, in dispatch
       new_user = await self._refresh_user(current_token)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     File 
"/usr/local/lib/python3.13/site-packages/airflow/api_fastapi/auth/middlewares/refresh_token.py",
 line 68, in _refresh_user
       return get_auth_manager().refresh_user(user=user)
              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
     File 
"/usr/local/lib/python3.13/site-packages/airflow/providers/keycloak/auth_manager/keycloak_auth_manager.py",
 line 121, in refresh_user
       tokens = client.refresh_token(user.refresh_token)
     File 
"/usr/local/lib/python3.13/site-packages/keycloak/keycloak_openid.py", line 
410, in refresh_token
       return raise_error_from_response(data_raw, KeycloakPostError)
     File "/usr/local/lib/python3.13/site-packages/keycloak/exceptions.py", 
line 195, in raise_error_from_response
       raise error(
       ...<3 lines>...
       )
   keycloak.exceptions.KeycloakPostError: 400: 
b'{"error":"invalid_grant","error_description":"Token is not active"}'
   ```
   
   
   ### What you think should happen instead
   
   When the KeycloakAuthManager tries to refresh the user and thus calls the 
refresh_token method from the Keycloak client and the later one fails with an 
KeycloakPostError, it should catch that exception and return None instead of 
propagating that exception as this makes the API server crash with HTTP 500 
Internal Server Error.
   
   Without that patch, the only way to fix this problem is to clear the cookies 
related to the API server and then refresh the page in the browser.
   
   The above fix or clearing the cookies as work around will force Keycloak to 
redo an authentication and thus refresh the token successfully instead of 
relying on an expired token in the cookie.
   
   ### How to reproduce
   
   Just keep the Airflow DAG's page open for a long time until the token 
expires on Keycloak side, then refresh it and you'll get an Internal Server 
Error page from the Airflow API server.
   
   <img width="577" height="250" alt="Image" 
src="https://github.com/user-attachments/assets/679a2242-f3fc-4cc1-8624-74e131ab59ce";
 />
   
   ### Anything else
   
   Before the fix
   
   When the API server received any request (e.g., GET /dags), Airflow’s 
refresh-token middleware tried to refresh the Keycloak token:
   
   tokens = client.refresh_token(user.refresh_token)
   
   
   If Keycloak returned invalid_grant (expired/invalid token), the 
python-keycloak client raised:
   
   KeycloakPostError: 400: {"error":"invalid_grant"...}
   
   
   This exception bubbled up unhandled, breaking the ASGI task group, causing:
   
   500 Internal Server Error
   ExceptionGroup: unhandled errors in a TaskGroup
   
   
   So instead of prompting a re-auth, the whole API crashed.
   
   After the fix
   
   You wrapped client.refresh_token(...) in a try/except KeycloakPostError:
   
   Catch the error
   
   Log a warning
   
   Return None instead of raising
   
   Because of that:
   
   ✔ The middleware no longer throws
   ✔ The API request is allowed to proceed
   ✔ A new login will be triggered on the next request (because None means 
“user is not authenticated anymore”)
   ✔ No more 500 errors from Uvicorn/Starlette
   
   In short:
   
   The fix prevents an unhandled Keycloak refresh error from crashing the API 
server. Instead, it logs the issue and gracefully forces re-authentication.
   
   ### Are you willing to submit PR?
   
   - [x] Yes I am willing to submit a PR!
   
   ### Code of Conduct
   
   - [x] I agree to follow this project's [Code of 
Conduct](https://github.com/apache/airflow/blob/main/CODE_OF_CONDUCT.md)
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to