Skip to content

Fix catch_exception crash when request.state is incomplete - #6853

Closed
juangalt wants to merge 1 commit into
keephq:mainfrom
juangalt:fix/catch-exception-missing-request-state
Closed

juangalt wants to merge 1 commit into
keephq:mainfrom
juangalt:fix/catch-exception-missing-request-state

Conversation

@juangalt

Copy link
Copy Markdown

Bug

The catch-all @app.exception_handler(Exception) in keep/api/api.py (catch_exception)
reads request.state.trace_id and request.state.tenant_id directly:

logging.error(
    f"An unhandled exception occurred: {exc}, Trace ID: {request.state.trace_id}. Tenant ID: {request.state.tenant_id}"
)

Starlette's State.__getattr__ raises AttributeError when an attribute was never set on
request.state. tenant_id is only assigned by LoggingMiddleware, and trace_id by an
earlier middleware layer — so if the original exception happens before either of those runs,
the handler itself crashes with a secondary AttributeError.

That secondary crash is worse than a cosmetic bug: it means

  1. Keep's own logging.error(...) call for the original exception never completes, so the
    original exception is not logged with a traceback (and, depending on log-pipeline routing,
    may not be logged at all), and
  2. because the handler raises instead of returning a response, Starlette's
    ServerErrorMiddleware never gets its return value back — which is what it needs in order
    to re-raise the original exception after the handler runs. The original failure is masked
    by the handler's own AttributeError.

Fix

  • Read both attributes with getattr(request.state, "...", None) instead of a direct attribute
    access, so a missing attribute degrades to None rather than crashing the handler.
  • Log via the module's existing logger.exception(...) instead of bare logging.error(...), so
    the original exception's traceback is preserved (logger.exception supplies exc_info
    automatically).
  • The JSON 500 response shape ({message, trace_id, error_msg}) and status code are unchanged;
    trace_id may now legitimately serialize as null when it was never set, which is expected.

Tests

Added tests/test_catch_exception_handler.py, which calls the registered handler directly
(test_app.exception_handlers[Exception]) against a bare Request/State for three cases:

  • tenant_id missing, trace_id present
  • both missing
  • both present

Each asserts HTTP 500, the expected JSON body (message/trace_id/error_msg), and — the
point of the fix — that no AttributeError is raised.

I verified this red-first: with the old body restored, the "tenant_id missing" and "both
missing" cases fail with AttributeError: 'State' object has no attribute '...' (the "both
present" case still passes unpatched, as expected, since both attributes exist in that case).
With the fix in place, all three pass.

This does not touch #6089 / #6100 (different tenantId surfaces) — it's scoped only to
catch_exception's handling of request.state.

…issing

Starlette's State.__getattr__ raises AttributeError when an attribute was
never set. catch_exception (the ASGI @app.exception_handler(Exception))
read request.state.trace_id / request.state.tenant_id directly, so when
a failure happened before the middleware that sets tenant_id (or even
trace_id) had a chance to run, the handler itself raised AttributeError.
That secondary failure discarded the intended logging.error(...) call and
prevented Starlette's ServerErrorMiddleware from re-raising the original
exception after the handler returns.

Use getattr(request.state, ..., None) for both attributes, and switch to
the module logger's .exception() (instead of bare logging.error) so the
original exception's traceback is preserved. The JSON 500 response shape
({message, trace_id, error_msg}) is unchanged.

Adds tests/test_catch_exception_handler.py covering tenant_id missing,
both missing, and both present.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@github-actions

Copy link
Copy Markdown
Contributor

Hey there and thank you for opening this pull request! 👋🏼

We require pull request titles to follow the Conventional Commits specification and it looks like your proposed title needs to be adjusted.

Details:

No release type found in pull request title "Fix catch_exception crash when request.state is incomplete". Add a prefix to indicate what kind of release this pull request corresponds to. For reference, see https://www.conventionalcommits.org/

Available types:
 - feat: A new feature
 - fix: A bug fix
 - docs: Documentation only changes
 - style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
 - refactor: A code change that neither fixes a bug nor adds a feature
 - perf: A code change that improves performance
 - test: Adding missing tests or correcting existing tests
 - build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
 - ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
 - chore: Other changes that don't modify src or test files
 - revert: Reverts a previous commit

@github-actions

Copy link
Copy Markdown
Contributor

No linked issues found. Please add the corresponding issues in the pull request description.
Use GitHub automation to close the issue when a PR is merged

@shahargl

Copy link
Copy Markdown
Member

seems like ai slop - no ticket, no reproduce, no evidence for user using Keep

@shahargl shahargl closed this Sep 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants