Skip to content

stream: honour StreamError.Retryable and retry transient upstream statuses - #368

Open
radamhub wants to merge 1 commit into
sirrobot01:betafrom
radamhub:fix/stream-transient-retry
Open

radamhub wants to merge 1 commit into
sirrobot01:betafrom
radamhub:fix/stream-transient-retry

Conversation

@radamhub

@radamhub radamhub commented Jul 30, 2026

Copy link
Copy Markdown

📌 Description

  • StreamError has a Retryable field that nothing ever reads, so transient upstream statuses end playback instead of being retried.

Reported in #345.


Target Branch Check (IMPORTANT)

  • I confirm this PR is targeting the correct branch

Expected target:

  • beta (for features)

Changes Made

  • Add StreamError.IsRetryable(). customerror.IsRetriableError probes for a selfRetryable interface (IsRetryable() bool, see internal/customerror/retry.go). StreamError did not implement it, so the Retryable field had no effect on any decision: it is set in three places in stream.go and read in none. One of those call sites builds StreamError{..., Retryable: true} and immediately wraps it in retry.Unrecoverable(...), which is self-contradictory.
  • In streamHTTP, stop classifying transient upstream statuses as unrecoverable. 429, 500, 502, 503 and 504 now return a retryable StreamError so the existing retry loops in pkg/mount/dfs/vfs/downloaders.go (DownloadWithRetry, downloadChunkWithRetry) can back off and try again. Every other unexpected status keeps the previous retry.Unrecoverable behaviour.
  • Add pkg/manager/stream_retry_test.go asserting both directions of the decision.

Testing

  • Tested locally

Steps:

  1. go test ./pkg/manager/ -run TestStreamErrorRetryabilityIsHonoured -v passes with the change, and fails without it (the retryable case is reported as non-retriable).
  2. Ran the patched build against a TorBox account for several days under a heavy import load, which produces 429s routinely. Before: a single 429 or 502 mid-stream ended the session: ffmpeg exits with an I/O error and the player keeps requesting segments that never arrive, so playback is dead until the client starts a new session. After: the log shows stream error, retrying ... attempt=1 and playback continues.
  3. Confirmed no regression on genuinely fatal statuses: a 416 still terminates the stream immediately.

Risks / Notes

  • Adding IsRetryable() makes the field meaningful, so it also changes behaviour at one pre-existing call site: StreamError{Err: ..., Retryable: true} returned after "connection retry exhausted" is now seen as retriable by the outer loop. That looks intended (the inner loop exhausting its own attempts does not mean the outer layer should give up), but it is a behaviour change worth a second opinion, and I am happy to gate that one differently if you prefer.
  • Retries are bounded by the existing loops, so this cannot spin indefinitely.
  • No config, API or on-disk format changes.

Checklist

  • Code builds successfully
  • No console/log errors
  • Reviewed my own code
  • Target branch is correct

…tuses

StreamError carries a Retryable field that is set in three places in
stream.go, but nothing ever reads it. customerror.IsRetriableError probes
for a selfRetryable interface (IsRetryable() bool), which StreamError does
not implement, so the field has no effect on any decision. One call site
even builds StreamError{..., Retryable: true} and immediately wraps it in
retry.Unrecoverable, which is self-contradictory.

Add the missing IsRetryable() method so the field reaches the retry loops
in pkg/mount/dfs/vfs/downloaders.go, and stop classifying transient
upstream statuses as unrecoverable while streaming: 429/500/502/503/504
are routine under load on debrid providers. Aborting on them ends playback
for the whole session, because ffmpeg sees an I/O error and the player
keeps requesting segments that never arrive.

Adds a unit test covering both directions of the retryability decision.
@radamhub
radamhub force-pushed the fix/stream-transient-retry branch from 4745eee to 65e49e8 Compare July 30, 2026 15:58

This branch has not been deployed

No deployments
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