Skip to content

Google Health commands: inconsistent/silent error handling #165

Description

@QueryPlanner

What

Found during a code-reduction audit of the Telegram Google Health command handlers in telegram/bot.py and health/service.py. Four related error-handling gaps.

  1. _connect_health has narrower exception handling than its three siblings (bot.py:697): it only catches (GoogleHealthOAuthError, ValueError) around service.begin_authorization(...), while _send_health_summary, _refresh_health, and _handle_health_callback all also catch broad Exception. Any other failure (e.g. a storage/DB error inside storage.store_oauth_state) propagates uncaught to the generic top-level handler in _safe_handle_update, which only logs and never messages the user — unlike the other three commands, which always reply with a specific error string.
  2. Undocumented invariant in the disconnect-confirmation callback (bot.py:808): query.from_user.id != chat.id is used to authorize the callback. This is true for Telegram private chats but is an implicit assumption with no comment explaining why it's safe — a future change to allow group-chat health commands would silently break this check.
  3. Token-revocation failure is silently swallowed and can misreport state (health/service.py, disconnect()): a GoogleHealthApiError during remote token revocation is caught and only logged (logger.warning), while local storage deletion still proceeds. A second /disconnect_health afterward reports "already disconnected" (storage-layer idempotency) even though the remote Google token may still be live and never actually revoked.
  4. notify_health_connection doesn't correlate the callback to a specific pending authorization (bot.py:836): it resolves chat_id from telegram_user_id alone with no check that this is the same chat/session that initiated /connect_health\| recently. Low likelihood of exploitation given consume_oauth_state's behavior, but worth confirming the OAuth state` correlation actually prevents a stale/replayed callback from notifying an unrelated chat.

Priority

Medium for item 1 (a real code path that can leave a user with zero feedback on failure) and item 3 (silently-live remote token is a minor security/privacy concern); Low for items 2 and 4 (robustness/documentation gaps, not confirmed exploitable).

Level of Effort

Small (S) for items 1 and 2 (add a matching except Exception clause; add a comment or an explicit chat-type/ID-equality helper). Medium (M) for item 3 (needs a decision on retry/surfacing behavior for revocation failures, plus a state field to track "revoke pending"). Small (S) for item 4 (verify/document the existing consume_oauth_state guarantee, or add an explicit check).

Sources

Passing criteria / definition of done

  • _connect_health catches the same exception surface as its siblings (broad Exception, with the existing (GoogleHealthOAuthError, ValueError) handling preserved for its specific messaging); a test simulates a generic storage failure and asserts the user receives an error message rather than silence.
  • The private-chat/user-ID-equality authorization check has an explicit comment (or a named helper function) documenting the invariant it relies on.
  • disconnect()'s behavior on revocation failure is either surfaced to the caller (e.g. a distinct return status) or the "already disconnected" message is changed to avoid implying full success when revocation failed; a test covers the revocation-failure path and asserts the returned/displayed state accurately reflects it.
  • A comment or test confirms consume_oauth_state prevents a replayed/stale OAuth callback from reaching notify_health_connection for an unintended chat.
  • pytest tests/test_telegram_health.py -q passes with the new/updated assertions.
  • ruff check, ruff format --check, and mypy src/blacki/ all pass with no new warnings.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions