fix(freesailarr): redirect the bare /qbittorrent prefix in Gateway mode
Validate Helm Charts / Detect changed charts (pull_request) Successful in 5s
Validate Helm Charts / Resource quantities (pull_request) Successful in 15s
Validate Helm Charts / Selector match (pull_request) Successful in 14s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 14s
Validate Helm Charts / Build freesailarr (pull_request) Successful in 8s
Validate Helm Charts / Trivy config freesailarr (pull_request) Successful in 22s
Install-test / Install-test freesailarr (pull_request) Successful in 4m19s
Validate Helm Charts / Install-test (pull_request) Successful in 4m19s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 12s
Publish Freesailarr Helm Chart / Publish Freesailarr Helm Chart (push) Successful in 12s
SonarQube Analysis / SonarQube Analysis (push) Successful in 48s

qBittorrent has no base-path setting, so the chart serves it under a prefix
and strips that prefix before the request reaches it. For the slashed form
that is correct. For the BARE form - /qbittorrent, no trailing slash - the
request is rewritten to "/" and qBittorrent answers 200 with its login page,
while the browser's document URL keeps the prefixless form. Every relative
link in that page therefore resolves against the host root, and on the shared
host the root is seerr: the login form comes up unstyled and does nothing
while a different application answers its assets and XHRs. Nothing reports an
error at any level.

The shared-domain HTTPRoute now carries a second rule for qBittorrent: an
Exact match on qbittorrent.ingress.path with a RequestRedirect filter that
replaces the full path with the slashed form, statusCode 301. The Gateway API
ranks an Exact path match above every prefix match, so it wins over both
qBittorrent's own PathPrefix rule and seerr's "/" wherever it is emitted - the
rules' length-descending order stays a rendering convention. 301 rather than
308 because only 301 and 302 are Core in that API; an Extended value an
implementation declines makes it set Accepted=False on the WHOLE route, which
would take the four neighbouring apps' routing down with it, and that would
also contradict decision 1 of Gitea #114. statusCode is written out because it
defaults to 302. The rule is guarded on the app being qbittorrent and its path
not being "/", and it reads the path from the value, never a literal.

The INGRESS BRANCH IS UNCHANGED, deliberately. The remedy there is an
nginx configuration-snippet annotation, and the chart cannot ship it: snippets
are ignored unless the controller runs with allow-snippet-annotations=true,
off by default since ingress-nginx 1.9 and only switched on by accepting
annotation risk level "Critical". A shipped annotation would be a silent no-op
on every cluster keeping the default. The working snippet, that price, and why
a third Ingress object with pathType: Exact is not a substitute - buildLocation
in ingress-nginx controller-v1.14.5 returns a regex location in its
enforceRegex branch before reaching the PathTypeExact branch, and enforceRegex
is computed once per nginx server over every location of the hostname, one of
which carries rewrite-target - are all written out in values.yaml instead.

ci/run-httproute-match-test.sh gains a third assertion, per HTTPRoute object:
a rule stripping a prefix with ReplacePrefixMatch requires a rule in the same
route matching that path Exactly and carrying a RequestRedirect filter. This
is what stops the gap reappearing; bulwark is the second rewrite-target chart
and meets the same check when group 3 of Gitea #114 lands. 37 assertions over
15 charts, up from 36. Both fail paths were exercised: the rule deleted, and
the redirect pointed at a path other than the one stripped.

Minor and not major: with the Ingress branch untouched and httpRoute.enabled
defaulting to false, no existing release's render changes by a byte and no
value becomes required. Shown by an empty helm template diff against
origin/main in both Ingress shapes, shared-domain and per-app hosts.

Fixes #118
This commit was merged in pull request #119.
This commit is contained in:
jklein committed 2026-10-12 20:06:09 +00:00
1 parent 471ea5ba5d
commit f202fdc3fc
6 files changed
+345 -23

No files matched your search

+38 -8
View File
@@ -12,6 +12,14 @@
{{- range $shared }}{{ $sharedNames = append $sharedNames .name }}{{ end -}}
{{- $httpRoute := .Values.httpRoute | default dict -}}
{{- $published := include "freesailarr.published" . -}}
{{- /* NOTHING IN CI READS THIS FILE, so every claim below is unverified by any
pipeline stage. `helm template` does not emit NOTES.txt at all, and no
workflow or script under ci/ reads it (measured for Gitea #118: the
three hits for "NOTES" under ci/ are comments in fixtures, not reads).
The render tests therefore cannot catch a statement here that has gone
stale, and the install-test never sees it either — it is prose checked
by review only. Treat an edit to it with the same care as the templates
it describes, because nothing else will. */ -}}
freesailarr — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
{{ if .Values.mediathekarr.enabled }}Ten{{ else }}Nine{{ end }} containers in ONE pod, sharing gluetun's network namespace.
@@ -87,17 +95,39 @@ ACCESS
same host, because nginx applies rewrite-target per object and the other
apps must not be rewritten.
{{- else }} In Gateway mode that second object is gone: a
URLRewrite filter belongs to a RULE, so qBittorrent is one rule of
URLRewrite filter belongs to a RULE, so qBittorrent is a rule of
{{ $name }}-httproute with the filter on it and its neighbours are
untouched by construction — no extra object, no regex path.
{{- end }}
Two things here look exactly
like a login failure and send you looking in the wrong place: its session
cookie is marked "Secure" (over plain HTTP the browser never sends it back
and every click bounces to the login form), and it bans the client IP for a
while after a few failed attempts. If it does not work at all, set
qbittorrent.ingress.host and it is back on a host of its own - no chart
change needed.
OPEN IT WITH THE TRAILING SLASH:
https://{{ $sharedHost }}{{ include "freesailarr.appPath" (dict "root" $ "app" "qbittorrent") }}/
{{ if .Values.ingress.enabled }} Without the slash the prefix is stripped to "/", qBittorrent answers 200,
and every RELATIVE link in that page resolves against the ROOT of this
host — which is {{ if has "seerr" $sharedNames }}seerr{{ else }}whatever serves "/" here{{ end }}, not qBittorrent. The login form then looks
unstyled and does nothing while another application answers its requests.
This chart does NOT render that redirect in Ingress mode: nginx offers only
a configuration-snippet annotation for it, and snippets are ignored unless
the controller runs with allow-snippet-annotations=true, which has been off
by default since ingress-nginx 1.9 and means accepting annotation risk
level "Critical". The working snippet, and its price, are in values.yaml at
qbittorrent.ingress (Gitea #118).
{{ else }} The bare, slashless path is redirected 301 to the slashed form by a rule of
{{ $name }}-httproute — an Exact path match, which the Gateway API ranks
above every prefix match, so it wins wherever it sits in the list. Without
it qBittorrent would answer 200 under a URL whose relative links resolve to
the root of this host, which is a different application (Gitea #118). This
is the one thing the Gateway mode does here that the Ingress mode cannot.
{{ end }} Two further things look exactly like a login failure and send you
looking in the wrong place: its session cookie is marked "Secure" (over
plain HTTP the browser never sends it back and every click bounces to the
login form), and it bans the client IP for a while after a few failed
attempts. If it does not work at all, set qbittorrent.ingress.host and it
is back on a host of its own - no chart change needed.
{{- end }}
{{- if not (has "seerr" $sharedNames) }}