fix(qbittorrent): tell a refused release apart from a failed submission - #906
Open
m4bard wants to merge 1 commit into
Open
fix(qbittorrent): tell a refused release apart from a failed submission#906m4bard wants to merge 1 commit into
m4bard wants to merge 1 commit into
Conversation
qBittorrent 5.2 answers HTTP 409 when the info-hash being submitted is already in its download list. That happens whenever one release satisfies more than one wanted book: the release is grabbed for the first book, and the search for the second scores the same release top and submits it again. QbittorrentAddWorkflow raised DownloadClientSubmissionException for every non-success status, so this was indistinguishable from a broken client. On the automatic search path the cycle logged an error and carried on. On POST /api/v1/download/search-and-download the exception reached the controller's catch-all and came back as a 500, so a user who pressed search on such a book got "Failed to search and download" and no way to tell that the release was already downloading. Reproduced on stock canary 1.3.4 with a stub indexer serving one series-wide release and a stub client answering 409 on a repeat submission. Three cycles, thirteen seconds apart, same info-hash each time. Written up with logs on Listenarrs#881. DownloadClientRejectedReleaseException narrows the existing submission exception rather than sitting beside it, so every current handler keeps its behaviour and only callers that want to skip need to know the type exists. Sonarr's DownloadClientRejectedReleaseException derives from ReleaseDownloadException the same way, and is thrown from QBittorrent.cs on HttpStatusCode.Conflict, from Sabnzbd.cs when the response carries no ids and from Nzbget.cs on a null response. The workflow raises it only on Conflict; every other status is still a plain submission failure. The controller answers a rejection the way the service already answers "no suitable download client found", with Success = false and the reason, rather than a server error. Deliberately not included: the search path does not learn anything from the rejection, so the next cycle will select the same release and be refused again. Stopping the repetition needs a record of which wanted items a release satisfies, which is the data model question on Listenarrs#881 and is not mine to answer. This makes the refusal honest, not silent. Tests: the 409 case, plus a control on 500 asserting it is still a plain submission failure and not a rejection, so the branch cannot be widened without a test going red. Confirmed the new test fails with the production change reverted. Full suite: 3120 passed, 0 failed, 130 skipped.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This does not close #881. That issue asks three design questions I cannot answer for you, and this is the part of it that does not depend on any of them.
What happens today
qBittorrent 5.2 answers HTTP 409 when the info-hash being submitted is already in its download list. That happens whenever one release satisfies more than one wanted book: the release is grabbed for the first book, and the search for the second scores the same release top and submits it again.
QbittorrentAddWorkflowraisedDownloadClientSubmissionExceptionfor every non-success status, so that was indistinguishable from a broken client. The two entry points then diverge, which I had wrong in the issue until I ran it:POST /api/v1/download/search-and-downloadit reaches the controller's catch-all and comes back as a 500, so pressing search on such a book gives "Failed to search and download" and no way to tell that the release is already downloadingI reproduced it on stock
ghcr.io/listenarrs/listenarr:canaryreporting 1.3.4, with a stub indexer serving one series-wide release and a stub client answering 409 on a repeat submission. Three cycles, thirteen seconds apart, same info-hash each time. Logs are on #881, and the two stubs are public if you want to run it yourself.What this changes
DownloadClientRejectedReleaseExceptionnarrows the existing submission exception rather than sitting beside it, so every current handler keeps its behaviour and only callers that want to skip need to know the type exists. Sonarr'sDownloadClientRejectedReleaseExceptionderives fromReleaseDownloadExceptionthe same way.The workflow raises it only on
Conflict. Every other status is still a plain submission failure, and there is a test on 500 asserting exactly that, so the branch cannot be widened without something going red.The controller answers a rejection the way the service already answers "no suitable download client found":
Success = falsewith the reason, rather than a server error.Sonarr, for whatever it is worth
Checked against
v5-developrather than from memory.DownloadClientRejectedReleaseExceptionis thrown fromClients/QBittorrent/QBittorrent.cs:95and:171onHttpStatusCode.Conflict, fromClients/Sabnzbd/Sabnzbd.cs:49when the response carries no ids, and fromClients/Nzbget/Nzbget.cs:48on a null response.ProcessDownloadDecisions.ProcessDecisionInternalcatches it at:213-224and returnsSkipped, and the switch arm at:112-115is a barebreak, so nothing is recorded.I should be precise about how deliberate that is: the exception is not named in
ProcessDownloadDecisions, it lands in the catch-all alongside anything that is not a client-unavailable or authentication error. The outcome is what I am describing rather than a case written for it.Three different 409s, and this is one of them
Worth saying plainly, because I conflated two of them myself when I filed #881.
DownloadController.cs:98, where anImportPendingrow that outlived its client item blocks a legitimate re-grab. It pulls the other way, the guard being too strict, and this change does not touch itregistration_recovery_pendingon stuck importsWhat this deliberately does not do
The search path learns nothing from the rejection, so the next cycle selects the same release and is refused again. Stopping the repetition needs a record of which wanted items a release satisfies, which is question 1 on #881 and is yours to decide. This makes the refusal honest rather than silent.
It also interacts with #882, which asks whether a rejected submission should write a
DownloadFailedhistory row. If you take both, a 409 probably should not write one, and #882's body says so.Tests
The 409 case, plus a control on 500 asserting it is still a plain submission failure and not a rejection. Confirmed the new test fails with the production change reverted. Full unfiltered suite: 3120 passed, 0 failed, 130 skipped.
One structural note: the catch went in the controller rather than
DownloadService.SearchAndDownloadAsync. Putting it in the service pushed that file to 522 lines against the 500-line budget, and extracting from it would have collided with #882, which already restructures the same file.