Honour a review decision over the match, and keep credentials out of error messages - #5
Conversation
…t of errors Ported from fix/robustness-audit, which found both of these but sits on a pre-release base. A decision now outranks the match result, and a Lidarr URL carrying basic-auth credentials no longer appears in error messages. Co-Authored-By: Claude <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe import flow now gives explicit review decisions precedence over automatic matches and skips rows without release group IDs. Lidarr errors redact embedded credentials while authenticated requests continue using the original URL, with tests covering both behaviors. ChangesImport selection precedence
Lidarr credential scrubbing
Estimated code review effort: 3 (Moderate) | ~20 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@chartarr/lidarr.py`:
- Around line 120-124: Update the LidarrError construction in the HTTP error
branch to avoid retaining raw r.text in its public body field. Store the
scrubbed response body instead, ensuring add_album() cannot reintroduce
credentials when interpolating e.body into duplicate-album errors.
- Around line 39-48: Update _safe_url to redact userinfo from both successfully
parsed URLs and ValueError fallback strings before returning them. Use
parts.netloc when rebuilding parsed URLs, removing any text through the final
“@” while preserving the host, port, path, query, and fragment; ensure malformed
authorities that raise during urlsplit are sanitized before the fallback is
returned and stored in self.shown.
- Around line 99-114: Update the exception handlers in the Lidarr request flow
to raise each sanitized LidarrError with suppressed exception chaining instead
of `from e`. Apply this to the ConnectionError, Timeout, invalid-URL, and
generic RequestException paths, preserving their existing sanitized messages and
ensuring formatted tracebacks do not expose credential-bearing request details.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: de4ac47c-51de-4a69-a0ee-a902e34b08ea
📒 Files selected for processing (5)
CHANGELOG.mdchartarr/cli.pychartarr/lidarr.pytests/test_cli.pytests/test_lidarr.py
| parts = urllib.parse.urlsplit(url) | ||
| host = parts.hostname | ||
| except ValueError: | ||
| return url | ||
| if not host: | ||
| return url # nothing parsed as a host; nothing to hide | ||
| if parts.port: | ||
| host = f"{host}:{parts.port}" | ||
| return urllib.parse.urlunsplit( | ||
| (parts.scheme, host, parts.path, parts.query, parts.fragment)) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
python - <<'PY'
from urllib.parse import urlsplit
assert urlsplit("http://admin:hunter2@").hostname is None
try:
urlsplit("http://admin:hunter2@[::1")
except ValueError:
pass
else:
raise AssertionError("expected malformed IPv6 authority to fail parsing")
PYRepository: alperien/chartarr
Length of output: 155
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Files:"
git ls-files | rg '(^|/)lidarr\.py$|errors|exceptions|shown' || true
echo
echo "chartarr/lidarr.py outline:"
ast-grep outline chartarr/lidarr.py --view compact || true
echo
echo "chartarr/lidarr.py relevant sections:"
cat -n chartarr/lidarr.py | sed -n '1,180p'
echo
echo "Search shown/raise_from/raise LidarrError:"
rg -n "shown\s*=|raise .*from|LidarrError|shown" chartarr/lidarr.pyRepository: alperien/chartarr
Length of output: 9600
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "chartarr/lidarr.py tail:"
cat -n chartarr/lidarr.py | sed -n '180,250p'
echo
echo "Python urlsplit edge-case verifier:"
python3 - <<'PY'
from urllib.parse import urlsplit, urlunsplit
cases = [
"http://admin:hunter2@",
"http://admin:hunter2",
"http://admin:hunter2@[::1",
"http://admin:hunter2@[::1]:80",
"http://admin:hunter2@host/path",
"http://host/path",
"./bad",
]
for url in cases:
try:
p = urlsplit(url)
print(url, "hostname=", p.hostname, "netloc=", p.netloc, "text=", repr(p.geturl()))
except Exception as e:
print(url, "exception", type(e).__name__, "=", str(e))
PYRepository: alperien/chartarr
Length of output: 3489
Redact malformed authorities before returning them.
urlsplit("http://admin:hunter2@") has hostname is None but still emits admin:hunter2@, so _safe_url() stores the credential-bearing string in self.shown. A malformed IPv6 authority with userinfo also raises before any scrubbing. Strip userinfo from parts.netloc for parsed URLs and from parse-error fallbacks before storing shown, otherwise connection/invalid-URL tracebacks can still expose credentials.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@chartarr/lidarr.py` around lines 39 - 48, Update _safe_url to redact userinfo
from both successfully parsed URLs and ValueError fallback strings before
returning them. Use parts.netloc when rebuilding parsed URLs, removing any text
through the final “@” while preserving the host, port, path, query, and
fragment; ensure malformed authorities that raise during urlsplit are sanitized
before the fallback is returned and stored in self.shown.
| except requests.ConnectionError as e: | ||
| raise LidarrError( | ||
| f"Can't reach Lidarr at {self.base} — is it running, and is the " | ||
| f"Can't reach Lidarr at {self.shown} — is it running, and is the " | ||
| f"URL right? (the address you use in your browser)") from e | ||
| except requests.Timeout as e: | ||
| raise LidarrError(f"Lidarr at {self.base} timed out.") from e | ||
| raise LidarrError(f"Lidarr at {self.shown} timed out.") from e | ||
| except (requests.exceptions.InvalidSchema, | ||
| requests.exceptions.MissingSchema, | ||
| requests.exceptions.InvalidURL) as e: | ||
| raise LidarrError( | ||
| f"{self.base} is not a URL Lidarr can be reached at — it needs " | ||
| f"{self.shown} is not a URL Lidarr can be reached at — it needs " | ||
| f"to start with http:// or https://") from e | ||
| except requests.RequestException as e: | ||
| raise LidarrError(f"Lidarr request failed: {e}") from e | ||
| # requests quotes the url it was given, credentials and all | ||
| raise LidarrError( | ||
| f"Lidarr request to {self.shown} failed: {self._scrub(e)}") from e |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
python - <<'PY'
import traceback
secret = "http://admin:hunter2@lidarr.test:8686"
try:
raise RuntimeError(secret)
except RuntimeError as cause:
try:
raise ValueError("sanitized") from cause
except ValueError as error:
assert secret in "".join(traceback.format_exception(error))
PYRepository: alperien/chartarr
Length of output: 155
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Files matching lidarr.py:\n'
fd -a 'lidarr\.py$' . || true
printf '\nRelevant snippets and imports:\n'
while IFS= read -r f; do
printf '\n=== %s ===\n' "$f"
wc -l "$f"
ast-grep outline "$f" --view compact || true
sed -n '1,150p' "$f" | cat -n
done < <(fd 'lidarr\.py$' .)
printf '\nSearch for LidarrError definition and tests involving LidarrError:\n'
rg -n "class LidarrError|LidarrError|requests\.ConnectionError|Lidarr request|format_exception|traceback" .Repository: alperien/chartarr
Length of output: 344
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Relevant snippets and imports:\n'
sed -n '1,150p' chartarr/lidarr.py | cat -n
printf '\nSearch for LidarrError definition and tests involving LidarrError:\n'
rg -n "class LidarrError|LidarrError|requests\.ConnectionError|Lidarr request|format_exception|traceback" .Repository: alperien/chartarr
Length of output: 9752
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import traceback
import urllib.parse
base = "http://admin:hunter2@lidarr.test:8686"
safe = "http://lidarr.test:8686"
try:
raise RuntimeError(f"connection refused to {base}")
except RuntimeError as cause:
try:
raise RuntimeError(f"Lidarr request to {safe} failed: connection refused to {base}") from cause
except RuntimeError as error:
text = "".join(traceback.format_exception(error))
print("contains_base:", base in text)
print("contains_safe:", safe in text)
print("contains_credentials:", "`@lidarr.test`" in text)
for line in text.splitlines():
if base in line or safe in line:
print("relevant_trace_line:", line)
PY
printf '\nTests around credential-bearing errors:\n'
sed -n '230,360p' tests/test_lidarr.py | cat -n
printf '\ncli prints traceback usage:\n'
rg -n "traceback|LidarrError|print\\(" chartarr/cli.py tests/test_lidarr.pyRepository: alperien/chartarr
Length of output: 803
Suppress the credential-bearing exception chain.
The sanitized Lidarr_error message hides credentials in LidarrError.args, but raising from e keeps the original requests exception in formatted tracebacks, including its credential-bearing URL. Raise from None for these paths and assert traceback.format_exception(type(e), e, e.__traceback__) preserves the sanitized message only.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@chartarr/lidarr.py` around lines 99 - 114, Update the exception handlers in
the Lidarr request flow to raise each sanitized LidarrError with suppressed
exception chaining instead of `from e`. Apply this to the ConnectionError,
Timeout, invalid-URL, and generic RequestException paths, preserving their
existing sanitized messages and ensuring formatted tracebacks do not expose
credential-bearing request details.
| if r.status_code >= 400: | ||
| raise LidarrError(f"HTTP {r.status_code}: {r.text[:300] or 'no details'}", | ||
| status=r.status_code, body=r.text) | ||
| # a proxy's error page can quote the request url back at us | ||
| raise LidarrError( | ||
| f"HTTP {r.status_code}: {self._scrub(r.text)[:300] or 'no details'}", | ||
| status=r.status_code, body=r.text) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Do not retain the raw proxy response body on LidarrError.
The displayed HTTP message is scrubbed, but body=r.text retains credentials. add_album() later inserts e.body into a new user-visible duplicate-album error, reintroducing a proxy-echoed password. Store the scrubbed body, or keep any raw body private and never interpolate it into errors.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@chartarr/lidarr.py` around lines 120 - 124, Update the LidarrError
construction in the HTTP error branch to avoid retaining raw r.text in its
public body field. Store the scrubbed response body instead, ensuring
add_album() cannot reintroduce credentials when interpolating e.body into
duplicate-album errors.
Version bump and changelog. No code changes; everything here landed in #4, #5, #6 and #7. Four fixes since 0.1.0, led by the review screen redrawing about 17,000 times a second while it waited for a keypress. Also: a skip or re-pick ignored on a row that also matched, one failed search request stranding the albums behind it, and basic-auth credentials appearing in error messages. Adds the u undo key and an idle-albums note when --search is off. The --search round trip between #4 and #6 is deliberately absent from the changelog: 0.1.0 already searched correctly, and nothing shipped in between, so there is no user-visible bug to report. The dead searchForNewAlbum flag's removal is recorded under Changed. Co-Authored-By: Claude <noreply@anthropic.com>
…error messages (#5) import_set took the automatic match whenever there was one and consulted the decision only otherwise, so a skip on a row that later matched was ignored and a re-pick lost to the matcher's own choice. A decision now outranks the match result, and a matched row with no release group id is dropped rather than failing at Lidarr's lookup. A Lidarr URL can carry basic-auth credentials for a reverse proxy, and the whole URL was quoted back in connection errors, timeouts, non-JSON replies and proxy error pages. The password is stripped from what is printed; the request still sends it. Both found by fix/robustness-audit and ported onto current main, which that branch predates. 13 tests, 135 passing.
Version bump and changelog. No code changes; everything here landed in #4, #5, #6 and #7. Four fixes since 0.1.0, led by the review screen redrawing about 17,000 times a second while it waited for a keypress. Also: a skip or re-pick ignored on a row that also matched, one failed search request stranding the albums behind it, and basic-auth credentials appearing in error messages. Adds the u undo key and an idle-albums note when --search is off. The --search round trip between #4 and #6 is deliberately absent from the changelog: 0.1.0 already searched correctly, and nothing shipped in between, so there is no user-visible bug to report. The dead searchForNewAlbum flag's removal is recorded under Changed.
…error messages (#5) import_set took the automatic match whenever there was one and consulted the decision only otherwise, so a skip on a row that later matched was ignored and a re-pick lost to the matcher's own choice. A decision now outranks the match result, and a matched row with no release group id is dropped rather than failing at Lidarr's lookup. A Lidarr URL can carry basic-auth credentials for a reverse proxy, and the whole URL was quoted back in connection errors, timeouts, non-JSON replies and proxy error pages. The password is stripped from what is printed; the request still sends it. Both found by fix/robustness-audit and ported onto current main, which that branch predates. 13 tests, 135 passing.
Version bump and changelog. No code changes; everything here landed in #4, #5, #6 and #7. Four fixes since 0.1.0, led by the review screen redrawing about 17,000 times a second while it waited for a keypress. Also: a skip or re-pick ignored on a row that also matched, one failed search request stranding the albums behind it, and basic-auth credentials appearing in error messages. Adds the u undo key and an idle-albums note when --search is off. The --search round trip between #4 and #6 is deliberately absent from the changelog: 0.1.0 already searched correctly, and nothing shipped in between, so there is no user-visible bug to report. The dead searchForNewAlbum flag's removal is recorded under Changed.
…error messages (#5) import_set took the automatic match whenever there was one and consulted the decision only otherwise, so a skip on a row that later matched was ignored and a re-pick lost to the matcher's own choice. A decision now outranks the match result, and a matched row with no release group id is dropped rather than failing at Lidarr's lookup. A Lidarr URL can carry basic-auth credentials for a reverse proxy, and the whole URL was quoted back in connection errors, timeouts, non-JSON replies and proxy error pages. The password is stripped from what is printed; the request still sends it. Both turned up while hardening the client. 13 tests, 135 passing.
Version bump and changelog. No code changes; everything here landed in #4, #5, #6 and #7. Four fixes since 0.1.0, led by the review screen redrawing about 17,000 times a second while it waited for a keypress. Also: a skip or re-pick ignored on a row that also matched, one failed search request stranding the albums behind it, and basic-auth credentials appearing in error messages. Adds the u undo key and an idle-albums note when --search is off. The --search round trip between #4 and #6 is deliberately absent from the changelog: 0.1.0 already searched correctly, and nothing shipped in between, so there is no user-visible bug to report. The dead searchForNewAlbum flag's removal is recorded under Changed.
…error messages (#5) import_set took the automatic match whenever there was one and consulted the decision only otherwise, so a skip on a row that later matched was ignored and a re-pick lost to the matcher's own choice. A decision now outranks the match result, and a matched row with no release group id is dropped rather than failing at Lidarr's lookup. A Lidarr URL can carry basic-auth credentials for a reverse proxy, and the whole URL was quoted back in connection errors, timeouts, non-JSON replies and proxy error pages. The password is stripped from what is printed; the request still sends it. Both turned up while hardening the client. 13 tests, 135 passing.
Version bump and changelog. No code changes; everything here landed in #4, #5, #6 and #7. Four fixes since 0.1.0, led by the review screen redrawing about 17,000 times a second while it waited for a keypress. Also: a skip or re-pick ignored on a row that also matched, one failed search request stranding the albums behind it, and basic-auth credentials appearing in error messages. Adds the u undo key and an idle-albums note when --search is off. The --search round trip between #4 and #6 is deliberately absent from the changelog: 0.1.0 already searched correctly, and nothing shipped in between, so there is no user-visible bug to report. The dead searchForNewAlbum flag's removal is recorded under Changed.
…error messages (#5) import_set took the automatic match whenever there was one and consulted the decision only otherwise, so a skip on a row that later matched was ignored and a re-pick lost to the matcher's own choice. A decision now outranks the match result, and a matched row with no release group id is dropped rather than failing at Lidarr's lookup. A Lidarr URL can carry basic-auth credentials for a reverse proxy, and the whole URL was quoted back in connection errors, timeouts, non-JSON replies and proxy error pages. The password is stripped from what is printed; the request still sends it. Both turned up while hardening the client. 13 tests, 135 passing.
Version bump and changelog. No code changes; everything here landed in #4, #5, #6 and #7. Four fixes since 0.1.0, led by the review screen redrawing about 17,000 times a second while it waited for a keypress. Also: a skip or re-pick ignored on a row that also matched, one failed search request stranding the albums behind it, and basic-auth credentials appearing in error messages. Adds the u undo key and an idle-albums note when --search is off. The --search round trip between #4 and #6 is deliberately absent from the changelog: 0.1.0 already searched correctly, and nothing shipped in between, so there is no user-visible bug to report. The dead searchForNewAlbum flag's removal is recorded under Changed.
Two real bugs found by
fix/robustness-audit, ported onto current main rather than merged.Why ported and not merged: that branch forked from
c152e39, before PR #3. Merging it in either direction revertsmonitor="existing"back tomonitor="none"(the headline v0.1.0 bug — every pushed album silently unmonitored), swaps content-derived state keys back to rank-based ones, and drops alias search and the live/compilation demotion entirely. The findings are worth having; the base underneath them is not.A skip was ignored on a row that also matched
import_settook the automatic match whenever there was one and only consulted your decision otherwise. So a row you skipped, which then matched on a later--rematch, went to Lidarr anyway — and a re-pick lost to the candidate the matcher had led with.A decision now outranks the match result: a skip is respected wherever it appears, an accept always wins, and an undone decision (
u, from #4) falls back to the match. A matched row carrying no release group id — possible in a state file from an older version — is dropped rather than failing at Lidarr's lookup.A Lidarr URL with credentials appeared in error messages
http://user:pw@hostis how you get through a reverse proxy that wants basic auth. The whole URL was quoted back in connection errors, timeouts, non-JSON replies, and any proxy error page that echoed the request. Errors get pasted into bug reports.The password is stripped from what's printed; the request is unchanged and still authenticates. Verified both ways — the message shows
http://lidarr.test:8686, the wire still carriesadmin:hunter2.The branch's other two claims were already fixed on main
Unreachableis main'sSearchUnavailable— same semantics, and main's also honoursRetry-Afterand treats all 4xx as a real answer.InvalidSchema/MissingSchemahandling the branch dropped (that's the "you typed127.0.0.1:8686withouthttp://" message).Its tied-rank fix is moot: main keys state by
sha1(artist+title), so ranks aren't keys at all.Verification
ruffcleanuv build+twine check --strictpassThe start-downloads redesign from that branch is deliberately not here — it rests on a claim about
searchForNewAlbumI'm verifying separately, and it deserves its own PR.Summary by CodeRabbit