Skip to content

fix: refresh stale provider download links on retry - #1006

Merged
rogerfar merged 1 commit into
rogerfar:mainfrom
jvvilar:fix/refresh-download-link-on-retry
Jul 9, 2026
Merged

fix: refresh stale provider download links on retry#1006
rogerfar merged 1 commit into
rogerfar:mainfrom
jvvilar:fix/refresh-download-link-on-retry

Conversation

@jvvilar

@jvvilar jvvilar commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes a download retry loop where Aria2 repeatedly fails with HTTP 403 because rdt-client keeps reusing an expired provider CDN signed URL stored in Download.Path.

When a download fails, Reset() clears Link but leaves Path unchanged. On the next attempt, UnrestrictLink runs again against the same stale Path. For providers like Premiumize where Unrestrict() is a no-op, nothing ever refreshes the URL — so the download retries forever (~1/sec) with the same dead link.

This PR re-fetches download links from the provider via GetDownloadInfos() when RetryCount > 0, matches the correct file, updates Path, and then proceeds with the normal unrestrict flow.

How I found this

I initially suspected a long filename was causing torrents to hang. Example file:

Yakuza.Fiancé.Raise.wa.Tanin.ga.Ii.S01E05.Young.Lady.Tsubaki.1080p.B-Global.WEB-DL.JPN.AAC2.0.H.265.MSubs-ToonsHub.mkv

That turned out not to be the issue — the full download path was ~247 characters and the target folder created fine on G:\tmp\sonarr\.

The real symptom was different: the download would briefly appear in Aria2, then vanish, and the retry count climbed at ~1 per second (600+ and still going). Logs showed:

Download reported an error: 22: The response status is not successful. status=403

Aria2 error 22 is an HTTP failure; 403 Forbidden means the CDN rejected the request.

Ruling out other causes

I first wondered if this was an Aria2 User-Agent issue (CDN allowing browsers but blocking aria2/1.x). That theory fell apart when I tested the exact URL rdt-client was using in a browser — it also returned 403.

The breakthrough was comparing two URLs for the same file (I'm on Premiumize, not Real Debrid — the app name is legacy but the CDN behaviour is the same):

rdt-client stored link (403 in browser and Aria2):

https://3-cdn2-ovh-fra.energycdn.com/cdn3sto/sillyweasel-sto/6a43df2070da50.18300376/426800987/1782832942/321060974b14f09ec7ab5e75e8f0e4257b69d432/9667fb75a88e25ecc0ebe80729ae96462df3b47830bd8d0c6afbb8164d6b0c94/Yakuza.Fianc%C3%A9.Raise.wa.Tanin.ga.Ii.S01E05.Young.Lady.Tsubaki.1080p.B-Global.WEB-DL.JPN.AAC2.0.H.265.MSubs-ToonsHub.mkv

Fresh link from Premiumize website (downloads fine):

https://3-cdn2-ovh-fra.energycdn.com/cdn3sto/sillyweasel-sto/6a43df2070da50.18300376/426800987/1782932034/321060974b14f09ec7ab5e75e8f0e4257b69d432/4664658a67d9a451ed3c30405705af60790d28fbbf83233b0bfc7819d439680c/Yakuza.Fianc%C3%A9.Raise.wa.Tanin.ga.Ii.S01E05.Young.Lady.Tsubaki.1080p.B-Global.WEB-DL.JPN.AAC2.0.H.265.MSubs-ToonsHub.mkv

Same host, same file path — but different timestamp (1782832942 vs 1782932034, ~27 hours apart) and different signature hash. The rdt-client URL was an older, expired signed CDN link.

Retry Torrent worked as a workaround because it deletes and re-adds the torrent, which triggers a fresh GetDownloadInfos() and stores new links.

Root cause

Provider CDN URLs are time-limited signed links. When downloads are first created, rdt-client stores the provider link in Download.Path (via DownloadInfo.RestrictedLink from GetDownloadInfos()).

On per-download retry, TorrentRunner calls Reset() which clears Link but does not update Path. The next UnrestrictLink call therefore uses the same expired URL.

For Premiumize this is especially visible because Unrestrict() is a no-op — it returns the link unchanged:

public Task<String> Unrestrict(Torrent torrent, String link)
{
    return Task.FromResult(link);
}

So the retry loop never gets a fresh signature. Real Debrid and other providers can hit the same class of problem whenever Path already holds a CDN URL that has expired and Unrestrict does not mint a new one.

Why fix all providers, not just Premiumize

All debrid clients follow the same download lifecycle in rdt-client:

  1. GetDownloadInfos() → store RestrictedLink in Download.Path
  2. UnrestrictLink() → call IDebridClient.Unrestrict(Path) → store result in Download.Link
  3. Downloader fetches Link
  4. On failure → Reset() (clears Link, keeps Path) → retry

The stale-Path problem is in this shared flow, not in a single provider implementation. Refreshing from GetDownloadInfos() on retry is provider-agnostic and safe: if the provider returns the same link, we skip the update; if it returns a fresh one, we use it.

What this PR changes

  1. DownloadLinkMatcher — matches a Download to a DownloadInfo from a fresh GetDownloadInfos() call by:

    • filename (Download.FileName)
    • URL-encoded filename segment from the existing Path
    • single-file torrent fallback
  2. UpdatePath — new method on the downloads data/service layer to persist an updated restricted link.

  3. Torrents.UnrestrictLink — when RetryCount > 0, calls TryRefreshDownloadPath() before unrestricting:

    • fetches current links from the provider
    • matches the download
    • updates Path if a newer link is available
    • then runs the existing Unrestrict + UpdateUnrestrictedLink flow
  4. Unit tests for DownloadLinkMatcher (filename match, URL segment match with encoded Unicode, single-file fallback, multi-file no-match).

Test plan

  • dotnet test server/RdtClient.Service.Test/RdtClient.Service.Test.csproj — 219 tests passed
  • Reproduce stale-link 403 on Premiumize; confirm automatic retry fetches a fresh link and download completes without Retry Torrent
  • Single-file torrent: confirm fallback matching still works
  • Smoke test with Real Debrid (or other provider) to confirm no regression on first attempt (RetryCount == 0 should not call refresh)

@rogerfar
rogerfar merged commit c33e3c3 into rogerfar:main Jul 9, 2026
1 check passed
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.

2 participants