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:
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.
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
Test plan
Follow RED, GREEN, REFACTOR.
- 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.
- 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.
- 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.
- 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
- 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.
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:ffmpeg,ffprobe, and Streamlink were present.failedstate./api/twitch/connection-statusstill returnedconnected=true,valid=true,source=database_manual, because the stored expiration timestamp was in the future.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:
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.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 showActive & Validwhile recordings fail withUnauthorized.Expected behavior
Proposed implementation
1. Resolve a recording token with live validation
Add a recording-specific token resolution path in
app/services/system/twitch_token_service.pyrather than relying only on the stored expiration timestamp.Suggested contract:
Behavior:
2. Make anonymous mode explicit in the Streamlink command builder
Extend
app/utils/streamlink_utils.pywith an explicit anonymous mode instead of relying on implicitNonebehavior.In anonymous mode:
--twitch-api-header.--twitch-supported-codecs=h264, regardless of global or per-streamer H.265/AV1 preferences.developkeeps credentials process-local; retain and test that invariant.3. Add one bounded runtime retry for auth rejection
In
app/services/recording/process_manager.py:TWITCH_AUTH_FALLBACK_TO_H264.Recognized auth failures should be centralized in a small classifier and include at least:
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-statusreturnvalid=false. The existing frontend will then render the correct status.Acceptance criteria
/api/twitch/connection-statusreturnvalid=false.Test plan
Follow RED, GREEN, REFACTOR.
tests/test_twitch_token_service.py:tests/utils/test_streamlink_utils.py:ProcessManager:Out of scope