Skip to content

Don't fail the request with a bare 500 on transient JWT refresh errors - #70057

Open
kjh0623 wants to merge 4 commits into
apache:mainfrom
kjh0623:fix/jwt-refresh-middleware-transient-500
Open

Don't fail the request with a bare 500 on transient JWT refresh errors#70057
kjh0623 wants to merge 4 commits into
apache:mainfrom
kjh0623:fix/jwt-refresh-middleware-transient-500

Conversation

@kjh0623

@kjh0623 kjh0623 commented Jul 17, 2026

Copy link
Copy Markdown

Problem

JWTRefreshMiddleware.dispatch runs outside the FastAPI/Starlette exception handlers, and its try/except only catches HTTPException and AuthManagerRefreshTokenExpiredException. Any other exception raised while resolving/refreshing the user propagates to the ASGI layer and surfaces as a bare HTTP 500 — for a request that may not have needed the refresh at all.

The concrete way we hit this in production: PgBouncer recycles a backend connection underneath the API server, and the next request's _refresh_user()resolve_user_from_token() → user lookup raises sqlalchemy.exc.OperationalError ("server closed the connection unexpectedly"). The UI shows "Internal Server Error"; a manual refresh usually succeeds. Related reports of the same pain point: #57859, discussion #59487. (#62153 fixed the poisoned-session cascade after such a drop; this PR addresses the request that hits the error itself.)

Fix

Add a final except Exception arm around the refresh block that treats the failure as "could not refresh this time":

  • log a warning with the traceback,
  • proceed without injecting the user — the route's own auth dependency revalidates the token exactly as it does today when the middleware doesn't inject, and
  • leave the cookie untouched (no forced logout), so a subsequent request refreshes normally once the backend recovers.

HTTPException / token-expiry semantics are unchanged — those still clear the cookie via the existing arm.

Testing

New unit test: _refresh_user raising a non-HTTP exception → request completes with the downstream response, no set-cookie header, warning logged. Existing middleware tests unchanged and passing.

We have been running the equivalent of this change in production (as a patch on 3.1.8) on a deployment serving a ~3,600-DAG instance; the recurring bare 500s on the auth path disappeared.


Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)

Generated-by: Claude Code following the guidelines


  • Read the Pull Request Guidelines for more information. Note: commit author/co-author name and email in commits become permanently public when merged.
  • For fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
  • When adding dependency, check compliance with the ASF 3rd Party License Policy.
  • For significant user-facing changes create newsfragment: {pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.

JWTRefreshMiddleware runs outside the FastAPI exception handlers and only
caught HTTPException / AuthManagerRefreshTokenExpiredException. Any other
exception raised while resolving or refreshing the user - notably an
OperationalError when the metadata DB connection is recycled underneath the
API server - propagated to the ASGI layer and surfaced as a bare HTTP 500,
even for requests that did not need the refresh at all.

Treat such errors as "could not refresh this time": log a warning, proceed
without injecting the user (the route's own auth dependency revalidates the
token), and leave the cookie untouched so a later request refreshes once the
backend recovers. HTTPException / token-expiry semantics are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kjh0623
kjh0623 requested a review from vincbeck as a code owner July 17, 2026 22:51
@boring-cyborg boring-cyborg Bot added the area:API Airflow's REST/HTTP API label Jul 17, 2026

@SameerMesiah97 SameerMesiah97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think there is a disconnect between the stated motivation behind this PR and the actual code. You appear to want to hande transient infrastructure failures (e.g. dropped DB connections), but the imeplementation and tests appear to broaden this to any exception from _refresh_user(). Is that intentional?

Comment thread airflow-core/src/airflow/api_fastapi/auth/middlewares/refresh_token.py Outdated
@kjh0623

kjh0623 commented Jul 20, 2026

Copy link
Copy Markdown
Author

Good question — the broad catch is intentional, but you're right that the observability trade-off deserves a callout.

Why except Exception rather than a specific OperationalError: this middleware is a generic auth layer that delegates the actual lookup to the configured auth manager (_refresh_userresolve_user_from_token). It doesn't — and shouldn't — know which backend that auth manager uses, so it can't enumerate which exceptions are "transient infra." Catching sqlalchemy.exc.OperationalError here would bake a DB-backend assumption into a layer that's meant to be backend-agnostic, and it would still let any other transient cause surface as the exact bare 500 this PR is trying to remove.

Why failing open is safe regardless of the exception type: the refresh is a best-effort, proactive step, not the authoritative auth check. If it doesn't inject a user, the route's own auth dependency revalidates the token exactly as it does today when the middleware chooses not to refresh. So a failure here is never a security downgrade — it just means "no proactive refresh this time," identical to the existing no-refresh path.

On the part I think is fair — masking programming errors: they aren't silently swallowed (the traceback is logged with exc_info=True), but WARNING is easy to miss, and a monitor keyed on 5xx would go quiet on a genuine regression. If you'd prefer, I'll switch the unexpected arm to log.exception(...) (ERROR level) so real bugs still surface loudly in monitoring while transient blips stay non-fatal. That keeps the resilience without hiding regressions.

Would that address the concern, or would you rather I narrow the catch despite the coupling/coverage downsides above?

@potiuk potiuk added the ready for maintainer review Set after triaging when all criteria pass. label Jul 20, 2026
kjh0623 and others added 3 commits July 21, 2026 21:05
Failing open on an unexpected error keeps the request alive, but it must not
make a genuine bug in the auth manager quieter than the bare 500 it replaces.
WARNING is easy to miss and a monitor keyed on 5xx would go silent on a real
regression, so switch the unexpected arm to log.exception() - ERROR level with
the traceback - while transient blips stay non-fatal (review feedback from
@SameerMesiah97).

Signed-off-by: kjh0623 <8412070+kjh0623@users.noreply.github.com>
Signed-off-by: kjh0623 <8412070+kjh0623@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:API Airflow's REST/HTTP API ready for maintainer review Set after triaging when all criteria pass.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants