Skip to content

fix: remove the last broad exception handlers, cover remaining invariants - #14

Closed
Blackwellcode wants to merge 7 commits into
masterfrom
audit/silent-failures
Closed

Blackwellcode wants to merge 7 commits into
masterfrom
audit/silent-failures

Conversation

@Blackwellcode

@Blackwellcode Blackwellcode commented Nov 12, 2026 •

Copy link
Copy Markdown
Owner

Summary

Removes the last 46 except Exception/bare: pass handlers (audit finding B5) and adds tests for the invariant functions that still had none (audit finding T1).

 abort_deleted_media(streamer, filename)
   try:
-    if database.is_deleted_media(...): abort(404)   # NotFound is an Exception ...
-  except Exception: pass                           # ... so the 404 was swallowed
+    deleted = database.is_deleted_media(...)
+  except sqlite3.Error: log warning, serve as before
+  if deleted: abort(404)
core/storage/http.py        close_quietly(): OSError/urllib3 HTTPError -> debug log   (15 handlers)
core/storage/base.py        rotated token not persisted -> warning                     (1)
core/storage/service.py     progress / provider-error bookkeeping -> debug / warning   (2)
core/twitch_api, eventsub   unreadable HTTP error body -> debug                        (5)
core/live_watch_diag, chat_chunker, redis_client, tube_auth -> warning                 (5)
WEBUI/webui.py              sqlite3.Error / OSError / GQL errors -> warning            (18)

Every fallback value stays the same. Only the listed error classes are absorbed; anything else now propagates.

Evidence

  • Before (origin/master 016090b): AST count of broad handlers whose body is only pass: 46. unittest discover -s tests: Ran 328 tests ... OK (skipped=20). A tombstoned VOD with its file still on disk: GET /vod/streamer/vod.mp4 -> 200.
    After (this branch): AST count 0. Ran 361 tests ... OK (skipped=23) (Windows; the POSIX tests run in CI). Same request -> 404 (DeletedMediaTombstoneTests.test_tombstoned_vod_page_is_404, red before the fix, green after).
  • Tests inject the expected failure for every changed function except three (sqlite3.OperationalError("database is locked"), ConnectionResetError, URLError, PermissionError, corrupt meta.json). Each checks that the fallback still comes back and that a log line is written: tests/test_error_handling.py (22 tests), plus StorageBookkeepingFailureTests in tests/test_storage.py. The three without a dedicated test: the chat-log title read in read_media_sidecar_metadata, the config lookups in twitch_watch, and twitch_eventsub.reload_env (the delete_state call there absorbs Redis errors itself).
  • New invariant tests in tests/test_invariants.py: lock-file PID ownership, the instance flock across processes (POSIX), SIGKILL fallback for a channel worker that ignores SIGTERM (POSIX), and traversal and symlink rejection in worker._safe_vod_task_path (17/17 lines covered, was 1/17).
  • Coverage, local Windows run (statements + branches): 34 % -> 37 %.
  • ruff check .: All checks passed. compileall: OK.

Merge Danger

Door: two-way

Plain revert. No schema, config or data changes.

Blast Radius: narrow

Narrowing except Exception means an unexpected error class (for example a TypeError from a malformed row) now shows up as an HTTP 500 or a traceback in the log, where it used to vanish. That is intended. Deleted VODs and clips whose file still exists now return 404, which was always the intended behaviour.

🤖 Generated with Claude Code

Blackwellcode and others added 7 commits November 12, 2026 15:54
Fifteen `try: response.close() except Exception: pass` blocks in the
storage HTTP layer and the Google Drive, Dropbox, OneDrive, WebDAV and S3
providers become http.close_quietly(). It catches only OSError (socket
already gone, requests' ConnectionError) and urllib3's HTTPError, logs
them at debug level with the provider name, and lets anything else
propagate. The transfer result is unchanged: closing only returns the
connection to the pool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- base: a failing config-changed callback (rotated OAuth token not
  persisted) is caught as StorageError/sqlite3.Error/OSError/ValueError and
  logged as a warning; the request still succeeds as before.
- service: a locked database while saving transfer progress is logged at
  debug level; a failure to record the provider error after a failed
  restore is logged as a warning and the restore error is still raised.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ss helpers

Ten `except Exception: pass` handlers outside storage and the web UI now
catch the specific failure and leave a log line; every fallback value is
unchanged:

- twitch_api/twitch_eventsub: an unreadable HTTP error body (OSError,
  http.client.HTTPException) is logged at debug level; the status code
  still decides the result. reload_env() calls redis_client.delete_state()
  directly because it already absorbs Redis errors.
- live_watch_diag: sqlite3.Error/ValueError reading channel configs is a
  warning; the diagnosis continues without the database fields.
- chat_chunker: an unreadable cached meta.json is a warning and the chunks
  are rebuilt.
- redis_client: failing to remove the dashboard shm cache is a warning.
- tube_auth: sqlite3.Error reading VOD access rows is a warning and the
  seed list is used, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
abort_deleted_media() called abort(404) inside `try ... except
Exception: pass`. werkzeug's NotFound is an Exception, so the handler
swallowed it and a tombstoned VOD or clip whose file was still on disk
(restore, failed unlink, re-sync) kept opening in the player.

The tombstone lookup now catches only sqlite3.Error, logs it and serves
as before; abort(404) runs outside the handler. The alias lookup in the
same function logs sqlite3.Error instead of passing silently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The last sixteen pass-only broad handlers in webui.py (plus the adjacent
channel-config lookup in twitch_watch) catch the failure they guard
against and log a warning; every fallback stays as it was:

- channel/alias lookups (can_view_streamer, resolve_media_path,
  delete_video_file, list_all_streamer_folders, _recording_logins_set,
  twitch_watch, _vod_access_candidate_slugs): sqlite3.Error, plus OSError
  where the archive folder or recording state is read.
- Twitch GQL profile/user-id lookups: OSError (URLError, timeouts),
  ValueError (bad JSON) and http.client.HTTPException. Unexpected answer
  shapes are parsed by _twitch_gql_user() instead of raising
  AttributeError into the handler.
- maintenance bypass check: OSError/ValueError, user stays not exempt.
- title from the chat log in read_media_sidecar_metadata: OSError.
- can_view_streamer no longer wraps normalize_channel_name(), which is
  pure string work and cannot fail.

AST count of `except Exception/bare: pass` outside tests: 46 -> 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…top and worker paths

Closes the remaining gaps from the test audit (06-tests 1.3 a/c/d):

- ChannelLockFileTests: the lock file carries the child PID after spawn;
  release_channel_lock() leaves a held or foreign lock alone unless the
  PID matches.
- InstanceLockRealFlockTests (POSIX): a second recorder instance exits
  with status 1 while another process holds the instance flock.
- StopChannelProcessRealChildTests (POSIX, fork): a channel worker that
  ignores SIGTERM is SIGKILLed within the deadline.
- WorkerVodTaskPathTests: _safe_vod_task_path() rejects traversal,
  absolute names, temp/non-archive files and symlinks out of the archive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Blackwellcode

Copy link
Copy Markdown
Owner Author

Superseded by #17 (rebased on master after #15; review fixes).

Blackwellcode added a commit that referenced this pull request Nov 12, 2026
fix: log every silently swallowed error, catch expected classes only (supersedes #14)
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