Skip to content

fix(connection): log unhandled RPC handler exceptions#116

Merged
michelTho merged 3 commits into
mainfrom
micheltho/log-unhandled-handler-exceptions
Jul 10, 2026
Merged

fix(connection): log unhandled RPC handler exceptions#116
michelTho merged 3 commits into
mainfrom
micheltho/log-unhandled-handler-exceptions

Conversation

@michelTho

@michelTho michelTho commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

What & why

Server-side handler crashes in Connection were converted to a wire error (requests) or silently dropped (notifications) without ever being logged, so integrators wiring an error reporter — e.g. Sentry via its logging integration — couldn't see them, forcing them to wrap every handler by hand. This makes both RPC handler paths log unhandled exceptions with their original traceback, consistent with every other error path in the module.

Changes

  1. _run_request — log the original exception (with traceback) before converting to JSON-RPC -32603; it previously did raise err from None, discarding the root-cause traceback.
  2. _run_notification — replace contextlib.suppress(Exception) (swallowed every notification error, no log, no response) with a try/except that logs. Drops the now-unused contextlib import.

Tests

tests/test_request_error_logging.py — deterministic unit tests drive
_run_request/_run_notification with a recording sender (asserting the exact wire
frame + that the original exception is logged). Verified the suite fails on the pre-fix silent-swallow behaviour.

Notes for review

  • Telemetry unchanged: suppress used to exit before span_context, so the span never saw the exception; catching inside try preserves that.
  • No protocol/wire change; no new data exposure.

Michel Thomazo and others added 3 commits July 8, 2026 16:36
Connection._run_request catches any unexpected handler exception, converts it
to a JSON-RPC -32603 response, and re-raises a fresh RequestError `from None` —
discarding the original exception and its traceback. Unlike every other error
path in this module (main_loop, the receive loop, stream observers,
_on_receive_error, _on_task_error), it is never logged, so server-side handler
crashes are invisible: integrators only ever see a contextless "Internal error"
on the wire with no stack to diagnose the root cause.

Log the original exception before converting it, mirroring the existing
logging.exception(..., exc_info=exc) calls in this file. Only the method name
and the exception (type + traceback) are logged, not the request params, and
str(exc) is already returned to the peer today — so this adds no new data
exposure. The protocol response is unchanged.

Adds a regression test: a handler that raises returns -32603 and the exception
is logged.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
Connection._run_notification wrapped the handler call in
contextlib.suppress(Exception), swallowing every notification-handler
error silently: no log, and (correctly) no wire response since
notifications have none by protocol. It was the only handler-error path
in the module that stayed silent — main_loop, the receive loop, stream
observers, _on_receive_error, _on_task_error, and (since the previous
commit) _run_request all log. Integrators wiring Sentry via the logging
integration therefore had to wrap every notification handler (cancel,
ext_notification, and on the client side session_update,
complete_elicitation) by hand to see failures.

Replace the suppress with a try/except that logs the original exception
and its traceback, mirroring the logging.exception(..., exc_info=exc)
call in _run_request. Behaviour is otherwise unchanged: the exception is
still not propagated, and the telemetry span still sees no exception
(contextlib.suppress already exited before span_context, so this is not a
span-status change). Unlike _run_request we deliberately do not re-raise:
notifications have no response and no store bookkeeping, so re-raising
would only add a duplicate, contextless "Background task failed" line.

Drop the now-unused contextlib import.

Adds a regression test: a session/cancel handler that raises is logged
(with traceback) instead of silently swallowed.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
Rewrites the request/notification error-logging tests to be deterministic
and precise instead of only asserting that "some record has exc_info":

- Drive Connection._run_request / _run_notification directly with a
  recording sender (mirroring test_core's TrackingSender) so assertions
  cover the exact wire frame: a request emits one -32603 error carrying
  {"details": ...} and re-raises RequestError; a notification emits
  nothing (deterministic, no timeout read).
- Assert the logged record is the original RuntimeError (type + message)
  under the expected method=..., not merely that a record exists.

Confirmed both tests fail on the pre-fix behaviour (silent suppress / no
request log) and pass with it.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
@michelTho michelTho force-pushed the micheltho/log-unhandled-handler-exceptions branch from 3d82436 to 1f0b15d Compare July 9, 2026 14:46
@michelTho michelTho requested a review from PsiACE July 9, 2026 14:50
@michelTho

Copy link
Copy Markdown
Contributor Author

Hey @PsiACE
A small PR to let errors be logged. It's interested: in our case it will allow to let sentry ingest them automatically (rather than having to set distinct exception handler for all requests / notifications)

Tell me if you need me to change anything, should be pretty harmless though

@michelTho michelTho merged commit 8a9af4b into main Jul 10, 2026
8 checks passed
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.

1 participant