fix(qbittorrent): add URL Base support for reverse-proxied instances - #772
fix(qbittorrent): add URL Base support for reverse-proxied instances#772schmitzkr wants to merge 3 commits into
Conversation
qBittorrent's download client only had Host/Port, so it always connected at the root path and couldn't reach instances served behind a path-prefixed reverse proxy (Transmission already had this field). Unlike Transmission's urlBase, which replaces the whole RPC path to match Transmission's own configurable --rpc-url-base daemon setting, qBittorrent has no equivalent server-side base path setting (upstream qbittorrent/qBittorrent#21471 and #23467 are both unmerged), so this prepends urlBase as a plain prefix before the fixed /api/v2/... routes instead of replacing them. Closes Listenarrs#690. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
I have actually stood up a Bindery instance to be able to have my downloads handled. But if this ever gets merged I think I might come back to Listenarr. |
|
I came to #690 meaning to implement it, found this, and read it instead. It is the better design and I would rather say why than duplicate it. Everything below is from checking out the branch and running it, not from reading the diff. The prefix-versus-replace decision is right, and it is checkableYou reasoned that qBittorrent has no server-side base path, so the prefix must be prepended before the fixed What I verifiedThe 8-file sweep is genuinely mechanical. Exactly eight one-line substitutions, and diffing the changed hunks shows no other line altered in those files. Afterwards, the only remaining
Full unfiltered backend suite on your branch: 1190 passed, 0 failed. Your five diacritics failures do not reproduce here, which supports your reading that they are an ICU-less sandbox artefact rather than anything you touched. It still merges cleanly into current canary, and it does not conflict with #906, which touches I think your Referrer note is the right observation pointing the wrong wayYou flagged that only one of the login implementations sets a When neither
None of that is a problem with this PR. Your change moves that header from the bare authority to the prefixed one, and since only host and port are compared, it still passes. One thing I would fix, and one I probably would notA full URL in the field is accepted and silently mangled. I ran the normalization over a few inputs: The first four are exactly right. The last one worries me because it is the likely paste: #690 describes the setup as Sonarr, Radarr and Readarr reject anything beginning with a scheme in their The other one I noticed is that On the disclosureFor what it is worth, the line-by-line read you said you had not done is the thing I have now done, and the code holds up. Flagging the split between direction and code-writing was the right call and it made this easy to review well. Checked against canary |
A value like https://seedbox.example.com/qbittorrent was silently concatenated onto the authority instead of being used as a path, producing a broken request URL with no indication why. Reject anything that parses as an absolute URI and surface a clear message through Test Connection instead. Follow-up to PR Listenarrs#772 per review feedback from m4bard.
A value like https://seedbox.example.com/qbittorrent was silently concatenated onto the authority instead of being used as a path, producing a broken request URL with no indication why. Reject anything that parses as an absolute URI and surface a clear message through Test Connection instead. Follow-up to PR Listenarrs#772 per review feedback from m4bard.
|
Thanks for this — genuinely one of the more thorough reviews I've had on this repo. The I've pushed a fix for the one open item: a value like In the same spirit of disclosure you raised: this fix, and the verification of your review's claims against the branch beforehand, was implemented by Claude Sonnet 5 via Claude Code, at my direction — I reviewed the diff and pushed it myself. |
|
Thanks for checking those against the branch rather than taking them on trust. That is the part that makes a review worth writing. The rejection does what you intended for a pasted full URL. It also rejects the value the field is documented to take, but only on Linux, so I would not expect it to have shown up for you. What I ranChecked out 82ced00 and ran the suite. Why
So On Windows Options, and I do not think it matters much whichChecking the scheme after parsing keeps the shape you have: if (Uri.TryCreate(trimmed, UriKind.Absolute, out var parsed)
&& (parsed.Scheme == Uri.UriSchemeHttp || parsed.Scheme == Uri.UriSchemeHttps))A plain Either way it seems worth a case in the test for On the disclosureAppreciated, and mine is the same: the review, the runs above and this comment were done with Claude Code at my direction. I read the diff and the failure myself before writing this. |
Uri.TryCreate(trimmed, UriKind.Absolute, out _) also matches a leading-slash path like /qbittorrent as an absolute file: URI on Unix (though not on Windows), so the guard added in 82ced00 was rejecting the exact placeholder value from this field's own help text. Reuse the existing DownloadClientUriBuilder.TryParseHttpOrHttpsAbsoluteUri helper, which checks the parsed scheme is actually http/https, instead of accepting any absolute URI. Found and diagnosed by m4bard via actually running the suite against 82ced00 (TestConnection_WithUrlBase_PrefixesApiPath failed on Linux, passed on Windows) rather than reasoning about it — verified here the same way: full QbittorrentAdapterTests run, 35/35 passed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012oUV8enYWXb9MPrwScE6to
|
Confirmed — you're right, and thanks for actually running it instead of trusting the reasoning (mine or otherwise). Pushed a fix: swapped that check for This time I actually ran the suite before pushing rather than reasoning about it from static analysis (which is exactly the gap that let this ship in the first place) — full One more disclosure note, since we're both doing this openly: my commits now carry a |
7d90355 to
bebed53
Compare
Summary
Adds a "URL Base" field to the qBittorrent download client (closes #690), so Listenarr can reach qBittorrent instances served behind a path-prefixed reverse proxy (e.g.
example.com/qbittorrent) — matching the field Transmission's client already has.I deliberately didn't just copy Transmission's mechanism. Transmission's
urlBasereplaces its whole RPC path, because that mirrors Transmission's own daemon setting (--rpc-url-base) — Listenarr just matches whatever arbitrary path the user configured server-side. qBittorrent has no equivalent: I checked upstream, and a native WebUI base-path setting has been proposed twice (qbittorrent/qBittorrent#21471, closed unmerged; #23467, still open/unmerged) — neither has shipped. So every real qBittorrent-behind-a-proxy deployment today works by the reverse proxy stripping a path prefix before it reaches qBittorrent, which always serves at its own root regardless. That means the correct semantics here are a prefix prepended before the fixed/api/v2/...routes, not a full-path override.Changes
Added
QBittorrentHelpers.BuildBaseUrl()— composes authority + normalizedurlBaseprefix from client settings; no-op when unset, so existing configs are unaffected/qbittorrent/api/v2/app/versionwhen configuredChanged
DownloadClientUriBuilder.BuildAuthority(client)now go through the new helper instead (mechanical, one line each — no other behavior change)Testing
dotnet build/dotnet format --verify-no-changescleanvue-tscclean, full suite 393/393 passNotes
AI assistance disclosure: this PR was developed with Claude Code's assistance - the implementation (the helper, the 8-file sweep, the frontend field, and the tests) was written by Claude Code. My role was directing the approach and validating the result: I pushed back on an initial draft that assumed we should just copy Transmission's urlBase mechanism, which prompted the investigation that found qBittorrent needs different (prefix, not replace) semantics - that upstream research and the resulting design decision are reflected above. I also manually tested this against my own qBittorrent instance behind a real reverse proxy and confirmed it resolves the actual problem reported in #690, which is real, live validation beyond the automated test suite. I have not done a line-by-line manual read of the full diff myself - flagging that plainly since this is my first PR on a project I don't maintain, and I'd rather be upfront about the split between direction/testing and code-writing than imply a level of manual review that didn't happen.
One unrelated thing noticed while auditing every place qBittorrent builds a URL: there are 5 separate, independently-written login implementations across these files (pre-existing, not introduced by this change), and only one of them sets a
Referrerheader that qBittorrent's CSRF check can care about. It shouldn't interact withurlBase(qBittorrent's CSRF/Referer check is origin-based, not path-based), so I left it alone here, but it seemed worth mentioning in case it's the cause of a future report like "Test Connection works but adding a torrent fails."