Skip to content

fix(recording): fall back to anonymous H.264 when Twitch authentication is invalid #834

Description

@Serph91P

Summary

Recordings currently fail completely when a stored Twitch browser token has been revoked or otherwise becomes invalid before its locally stored expiration timestamp. StreamVault reports the token as valid, passes it to Streamlink, and does not retry anonymously with H.264 after Twitch rejects it.

The recording path must fail open to anonymous H.264 so capture reliability does not depend on optional Twitch authentication.

Production evidence

Observed on 2026-09-01 in the production Docker deployment running image revision 9dc68147b59d0f0a4275fe2002bac56b193b9506:

  • The container was healthy with no restarts.
  • The recording mount was writable and had about 7 TB free.
  • ffmpeg, ffprobe, and Streamlink were present.
  • The latest 11 recording rows were all in failed state.
  • The latest failed recording started a Streamlink process but wrote no segment file.
  • The Streamlink log reported:
Unauthorized: The "Authorization" token is invalid.
No playable streams found on this URL
  • /api/twitch/connection-status still returned connected=true, valid=true, source=database_manual, because the stored expiration timestamp was in the future.
  • A direct live Twitch validation of the same stored token returned invalid.
  • A bounded anonymous H.264 recording in the same container and on the same recording mount succeeded and produced a valid MPEG-TS file with H.264 video and AAC audio.

This isolates the failure to token handling and fallback behavior, not Docker health, Twitch reachability, Streamlink, or storage.

Root cause

Two behaviors combine to make this a recording outage:

  1. TwitchTokenService.get_valid_access_token() treats a manual database token as usable based on the local expiration timestamp. A token revoked before that timestamp is not rejected before recording starts.
  2. ProcessManager._start_segment() passes the selected token and configured codec preferences to Streamlink. If Twitch rejects the token, there is no bounded retry without authentication and with H.264 forced.

The frontend is not independently validating the token. It renders the backend result from /api/twitch/connection-status, so the UI can show Active & Valid while recordings fail with Unauthorized.

Expected behavior

  • A missing, expired, revoked, unverifiable, or Streamlink-rejected Twitch token must not prevent a recording.
  • StreamVault should fall back to an anonymous H.264 recording on the same proxy/direct route and output path.
  • Valid authenticated recordings should continue using the configured codec preference.
  • Authentication fallback must be bounded to one retry and must never loop.

Proposed implementation

1. Resolve a recording token with live validation

Add a recording-specific token resolution path in app/services/system/twitch_token_service.py rather than relying only on the stored expiration timestamp.

Suggested contract:

@dataclass(frozen=True)
class RecordingTokenResolution:
    token: str | None
    source: str | None
    live_valid: bool
    definitive_invalid: bool

Behavior:

  • Live-validate a candidate token immediately before starting a recording, with a short bounded timeout.
  • Return the token only when Twitch confirms it is valid.
  • On a definitive invalid response, mark the stored manual token invalid so the existing connection-status endpoint no longer reports it as valid.
  • On timeout or a transient validation failure, do not delete or overwrite the stored token, but use anonymous H.264 for that recording attempt.
  • Never log the token, authorization header, hashes, or encrypted value.

2. Make anonymous mode explicit in the Streamlink command builder

Extend app/utils/streamlink_utils.py with an explicit anonymous mode instead of relying on implicit None behavior.

In anonymous mode:

  • Do not add --twitch-api-header.
  • Force --twitch-supported-codecs=h264, regardless of global or per-streamer H.265/AV1 preferences.
  • Preserve the selected proxy or direct route, quality, output path, segmentation settings, and logfile behavior.
  • Ensure static config cannot reintroduce an authentication header. Current develop keeps credentials process-local; retain and test that invariant.

3. Add one bounded runtime retry for auth rejection

In app/services/recording/process_manager.py:

  • Start with the validated authenticated command when a usable token exists.
  • If Streamlink exits before producing recording data and the sanitized process output or Streamlink log contains a recognized authentication rejection, retry the same segment exactly once in explicit anonymous H.264 mode.
  • Do not retry anonymously for offline streams, proxy errors, disk errors, invalid output paths, or unrelated Streamlink failures.
  • Do not create a second recording row, duplicate segment bookkeeping, duplicate notifications, or leave the failed first process registered.
  • Record a structured, token-free event such as TWITCH_AUTH_FALLBACK_TO_H264.

Recognized auth failures should be centralized in a small classifier and include at least:

Unauthorized: The "Authorization" token is invalid.
401 Unauthorized

4. Keep the UI status truthful after definitive rejection

No new frontend flow is required for this ticket. After a definitive Twitch rejection, backend state must make /api/twitch/connection-status return valid=false. The existing frontend will then render the correct status.

Acceptance criteria

  • A manual token with a future local expiry but a failed live Twitch validation starts recording anonymously with H.264.
  • A transient validation timeout also starts anonymously with H.264 without deleting the stored token.
  • A valid token keeps the authenticated path and configured codec preference.
  • An auth-specific Streamlink startup failure retries exactly once without authentication and with H.264 forced.
  • A non-auth Streamlink failure does not trigger the auth fallback.
  • The fallback reuses the original proxy/direct selection and output path.
  • Only the successful process is registered for monitoring and segment bookkeeping.
  • No token or authorization header appears in application or Streamlink logs.
  • A definitive invalid token makes /api/twitch/connection-status return valid=false.
  • No database migration and no frontend source change are required.

Test plan

Follow RED, GREEN, REFACTOR.

  1. Extend tests/test_twitch_token_service.py:
    • future local expiry plus live validation failure returns an anonymous resolution;
    • definitive invalid state is persisted;
    • timeout/transient failure does not clear stored credentials;
    • valid token remains usable.
  2. Extend tests/utils/test_streamlink_utils.py:
    • anonymous mode omits every auth header;
    • anonymous mode forces H.264;
    • valid authenticated mode preserves configured codecs and proxy settings.
  3. Add focused async tests for ProcessManager:
    • auth rejection before output triggers one anonymous retry;
    • successful fallback registers only the replacement process;
    • non-auth failure and second failure stop without another retry;
    • command and logs never expose the token.
  4. Run the focused tests, then CI-equivalent backend gates:
pytest tests/test_twitch_token_service.py tests/utils/test_streamlink_utils.py tests/services/recording/test_process_manager_auth_fallback.py -v
ruff check app/ tests/
ruff format --check app/ tests/
pytest tests/ -v --cov=app --cov-report=term-missing --tb=short
python -m app.migrations_init
  1. Build the Docker image and run a container smoke test with a deliberately invalid token. Verify that a live channel produces a growing H.264/AAC recording file and that logs contain the fallback event but no credential material.

Out of scope

  • Automating Twitch website login, cookies, password, or 2FA.
  • Making H.265/AV1 available without a valid Twitch website token.
  • Redesigning the full OAuth settings UI.
  • Installing Chromium solely for client-integrity token generation.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:backendPython/FastAPIbugSomething isn't workingpriority:critical🔴 Critical priority - blocking productionpythonPull requests that update python codetype:bugBug fix

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions