Skip to content

fix(link): treat provider 400 as refetchable (expired presigned link) - #402

Open
amirldn wants to merge 1 commit into
sirrobot01:betafrom
amirldn:fix/400-refetchable-link
Open

amirldn wants to merge 1 commit into
sirrobot01:betafrom
amirldn:fix/400-refetchable-link

Conversation

@amirldn

@amirldn amirldn commented Aug 29, 2026

Copy link
Copy Markdown

AI Note

This PR was written fully by Opus 5.

  • I had my agent find this on my behalf as I was running into issues using TorBox with Decypharr (previously came from RD where decypharr was flawless).
  • The fix is running in production on my own server on a locally-built
    image, and it resolved the exact issue I was having (details under Testing).
  • The change is deliberately minimal.

📌 Description

ErrorCodeToLinkError has no case for 400, so it falls through to the
default: permanent branch. TorBox returns a bare 400 when a presigned
download link has expired or rotated — a refetchable condition — so a merely
stale link is treated as a dead file.


Target Branch Check (IMPORTANT)

  • I confirm this PR is targeting the correct branch

Expected target:

  • beta (for features)

Changes Made

  • Map error code 400 to NewRefetchableError in ErrorCodeToLinkError
  • Add an ErrLinkRejected sentinel alongside the existing Err404/Err429/Err503

This makes the provider-API path agree with the CDN path, which already gets
this right — ClassifyStreamStatus classifies 400 as refetchable with the
comment "4xx auth-shaped statuses and 404 usually mean the presigned link
expired or rotated"
. Only the validateLink HEAD path disagreed.

Why permanent is harmful here

countErrors fast-trips the circuit breaker on a permanent error:

if nntp.IsArticleNotFoundError(err) || customerror.IsPermanentError(err) {
    dls.errorCount = maxErrorCount   // straight to 10, no retries
}

and fetchAndValidate memoizes the result in s.validated, where only
ShouldRefetch() invalidates it. So one stale link makes the file read
0 bytes until cooldown instead of just refetching. Symptom in the *arrs is
Unable to determine if file is a sample / stuck importPending, because
the sample check can't read byte 0.


Testing

  • Tested locally

Steps:

  1. Confirmed TorBox returns 400 specifically for a bad/expired presigned link,
    while the torrent itself is healthy (myliststate=cached,
    download_present=true):

    request result
    fresh requestdl link, bytes 0-1MB 206, 1048576 bytes
    same link, UUID mangled 400
    same link, token stripped 400 missing field 'token'
  2. Added a temporary unit test on ErrorCodeToLinkError: it fails on current
    beta (400 must be refetchable, got category permanent) and passes with
    this change, with 404/429/unknown-code behaviour unchanged. Removed
    before committing.

  3. Built the image from this branch and ran it against a live TorBox account.
    Two files that previously returned Input/output error / 0 bytes now read
    at full speed (2.6 MB/s and 3.9 MB/s), and 400: unknown error code: 400
    went to zero across the whole mount. Remaining errors under a deliberate
    read burst were 429, which retries correctly.


Risks / Notes

  • Not a breaking change. Only reclassifies 400; all other codes unchanged.
  • A genuinely malformed request that returns 400 will now cost one refetch
    before failing, rather than failing immediately. That is bounded by the
    existing attempt budget in fetchAndValidate, and matches how the CDN
    layer has always handled 400.
  • Related: Retry link validation on read_pxy_timeout and other retryable error #384 fixed the same class of misclassification for
    read_pxy_timeout.

Checklist

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

ErrorCodeToLinkError has no case for 400, so it falls through to the
permanent default. TorBox returns a bare 400 when a presigned download
link has expired or rotated, which is a refetchable condition.

A permanent classification fast-trips the VFS circuit breaker
(errorCount = maxErrorCount) and is memoized in s.validated, where only
ShouldRefetch() invalidates it, so one stale link makes the file read
0 bytes until cooldown instead of refetching.

This aligns the provider-API path with ClassifyStreamStatus, which
already treats 400 at the CDN layer as refetchable.

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