Skip to content

fix(rpc): detect 401 auth errors and show actionable credential message#783

Merged
lklimek merged 2 commits into
v1.0-devfrom
fix/rpc-auth-401-detection
Mar 23, 2026
Merged

fix(rpc): detect 401 auth errors and show actionable credential message#783
lklimek merged 2 commits into
v1.0-devfrom
fix/rpc-auth-401-detection

Conversation

@lklimek
Copy link
Copy Markdown
Contributor

@lklimek lklimek commented Mar 23, 2026

Summary

  • Detect HTTP 401 (Unauthorized) from Dash Core RPC and show "Dash Core rejected your credentials. Check your RPC username and password in settings." instead of the generic "Could not communicate with Dash Core" or "Disconnected — check that Dash Core is running"
  • Add is_rpc_auth_error() helper that downcasts through the boxed transport error chain to identify HttpErrorCode(401)
  • Add TaskError::CoreRpcAuthFailed variant with user-friendly message
  • Refactor get_best_chain_lock() to preserve typed errors (Result<Option<ChainLock>, dashcore_rpc::Error>) instead of stringifying them, so auth errors on the active network surface through the task error system

User story

Imagine you are configuring Dash Evo Tool to connect to your local Dash Core node. You accidentally enter the wrong RPC password. Previously, the app would just say "Disconnected — check that Dash Core is running" — leaving you chasing a non-problem. Now the app tells you directly that your credentials were rejected and to check your username and password in settings.

Test plan

  • rpc_http_401_converts_to_core_rpc_auth_failed — verifies From conversion
  • rpc_http_401_display_mentions_credentials — verifies user-facing text
  • is_rpc_auth_error_detects_401 — unit test for helper
  • is_rpc_auth_error_ignores_other_http_codes — 403 does not false-positive
  • is_rpc_auth_error_ignores_rpc_errors — JSON-RPC errors do not false-positive
  • Manual: configure wrong RPC password, verify banner shows credential message
  • Manual: stop Dash Core, verify original "check that Dash Core is running" message still appears

🤖 Co-authored by Claudius the Magnificent AI Agent

Summary by CodeRabbit

  • Bug Fixes

    • Improved error handling for RPC authentication failures with clearer error messages.
  • New Features

    • Added character limit constraints to password input fields for better usability.
    • Enhanced network configuration screen with improved password input constraints.

When Dash Core RPC rejects with HTTP 401 (wrong password), the app
previously showed generic "Disconnected" or "Could not communicate
with Dash Core." Now it detects the 401 via downcast on the transport
error and shows: "Dash Core rejected your credentials. Check your RPC
username and password in settings."

- Add `is_rpc_auth_error()` helper that downcasts through
  `jsonrpc::error::Error::Transport` to `simple_http::Error::HttpErrorCode(401)`
- Add `TaskError::CoreRpcAuthFailed` variant with user-friendly message
- Check auth before wallet-not-specified in `From<dashcore_rpc::Error>`
- Refactor `get_best_chain_lock()` to return typed error instead of String,
  enabling auth detection in the `GetBestChainLocks` handler
- Add 5 unit tests covering detection, display, and edge cases

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 23, 2026

📝 Walkthrough

Walkthrough

The pull request improves RPC error handling by explicitly detecting HTTP 401 authentication failures and propagating them distinctly, while converting other configuration and connection errors to Ok(None). It also adds a character limit feature to password input fields with a builder method.

Changes

Cohort / File(s) Summary
RPC Error Handling
src/backend_task/core/mod.rs, src/backend_task/error.rs
Modified get_best_chain_lock() to return Result<Option<ChainLock>, dashcore_rpc::Error> and convert config/connection failures to Ok(None). Added TaskError::CoreRpcAuthFailed variant and is_rpc_auth_error() helper to detect and handle HTTP 401 auth errors explicitly. Updated error conversion to prioritize 401 detection before existing mappings.
Password Input Validation
src/ui/components/password_input.rs, src/ui/network_chooser_screen.rs
Added optional char_limit field to PasswordInput with consuming builder method with_char_limit(). Applied 40-character limit and 280.0 width to the dashmate password input in network chooser screen.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Whisker-twitched, the RPC gates now guard with care,
When 401 knocks, we know auth's not there,
Passwords now count each letter that's typed,
Forty chars max—clean, controlled, and hyped!
Error flows clearer, the system runs tight,
All aboard for a more robust flight! 🚀

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(rpc): detect 401 auth errors and show actionable credential message' directly and clearly describes the main change: detecting HTTP 401 RPC authentication errors and providing a user-facing credential message.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/rpc-auth-401-detection

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

- Drop "username" from error message — only password is user-configurable
- Add `with_char_limit()` builder to `PasswordInput` component
- Set 40-char limit and 280px width on network settings RPC password field

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves the UX around Dash Core RPC authentication failures by detecting HTTP 401 responses and surfacing an actionable, credential-focused error instead of generic connectivity messaging.

Changes:

  • Added a dedicated TaskError::CoreRpcAuthFailed and detection helper is_rpc_auth_error() to classify RPC HTTP 401 errors.
  • Refactored Core chain-lock fetching to preserve typed dashcore_rpc::Error results so auth failures can be surfaced on the active network.
  • Extended the PasswordInput component with optional character limits and applied it to the Core RPC password field in the network chooser UI.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
src/ui/network_chooser_screen.rs Adds width + character limit configuration to the Core RPC password input field.
src/ui/components/password_input.rs Introduces char_limit support and applies it to the underlying egui TextEdit.
src/backend_task/error.rs Adds CoreRpcAuthFailed, implements is_rpc_auth_error(), and tests 401 classification/display.
src/backend_task/core/mod.rs Refactors chain-lock retrieval to keep typed errors and surfaces auth failures for the active network.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/ui/network_chooser_screen.rs
Comment thread src/backend_task/error.rs
@lklimek lklimek marked this pull request as ready for review March 23, 2026 10:55
@lklimek lklimek enabled auto-merge (squash) March 23, 2026 10:55
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/ui/network_chooser_screen.rs (1)

124-127: Extract input policy literals into named constants.

Line 126 and Line 127 use hard-coded UI policy values; constants make intent clearer and easier to update.

Suggested refactor
 impl NetworkChooserScreen {
+    const LOCAL_RPC_PASSWORD_MAX_LEN: usize = 40;
+    const LOCAL_RPC_PASSWORD_INPUT_WIDTH: f32 = 280.0;
+
     pub fn new(
         mainnet_app_context: &Arc<AppContext>,
         testnet_app_context: Option<&Arc<AppContext>>,
         devnet_app_context: Option<&Arc<AppContext>>,
         local_app_context: Option<&Arc<AppContext>>,
@@
         let mut dashmate_password_input = PasswordInput::new()
             .with_hint_text("Core RPC password")
-            .with_char_limit(40)
-            .with_desired_width(280.0);
+            .with_char_limit(Self::LOCAL_RPC_PASSWORD_MAX_LEN)
+            .with_desired_width(Self::LOCAL_RPC_PASSWORD_INPUT_WIDTH);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/network_chooser_screen.rs` around lines 124 - 127, The PasswordInput
initialization in dashmate_password_input uses hard-coded UI policy values;
extract those literals into named constants (e.g., DASHMATE_PASSWORD_CHAR_LIMIT
and DASHMATE_PASSWORD_DESIRED_WIDTH) and replace the numeric literals in the
PasswordInput builder chain (.with_char_limit(40) and
.with_desired_width(280.0)) with those constants; declare the constants near the
top of the module (or next to related UI constants) so intent and
maintainability are clearer while keeping the variable name
dashmate_password_input and the PasswordInput::new() builder usage unchanged.
src/ui/components/password_input.rs (1)

84-87: Consider guarding with_char_limit(0) to avoid unusable input.

Line 84 currently accepts 0, which can effectively block input. A small guard keeps behavior safer.

Suggested tweak
 pub fn with_char_limit(mut self, limit: usize) -> Self {
-    self.char_limit = Some(limit);
+    self.char_limit = if limit == 0 { None } else { Some(limit) };
     self
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/components/password_input.rs` around lines 84 - 87, The
with_char_limit method currently allows calling with 0 which makes the input
unusable; update the function (pub fn with_char_limit) to guard against limit ==
0 by not setting self.char_limit (leave it as None) or otherwise ignoring the
call when limit is zero, and only set self.char_limit = Some(limit) for limit >=
1 so PasswordInput retains usable behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/backend_task/error.rs`:
- Around line 59-61: The CoreRpcAuthFailed TaskError variant currently drops the
underlying dashcore_rpc::Error; update the enum variant to include a typed
source field (e.g., CoreRpcAuthFailed { #[source] source: dashcore_rpc::Error })
and update any conversion/From implementations that map dashcore_rpc::Error ->
TaskError (or the caller that constructs CoreRpcAuthFailed) to pass the original
error through into that source field so the error chain is preserved when
Debug/Display are printed; apply the same change for the other TaskError
variants noted (lines ~733-735) that wrap dashcore_rpc::Error.

---

Nitpick comments:
In `@src/ui/components/password_input.rs`:
- Around line 84-87: The with_char_limit method currently allows calling with 0
which makes the input unusable; update the function (pub fn with_char_limit) to
guard against limit == 0 by not setting self.char_limit (leave it as None) or
otherwise ignoring the call when limit is zero, and only set self.char_limit =
Some(limit) for limit >= 1 so PasswordInput retains usable behavior.

In `@src/ui/network_chooser_screen.rs`:
- Around line 124-127: The PasswordInput initialization in
dashmate_password_input uses hard-coded UI policy values; extract those literals
into named constants (e.g., DASHMATE_PASSWORD_CHAR_LIMIT and
DASHMATE_PASSWORD_DESIRED_WIDTH) and replace the numeric literals in the
PasswordInput builder chain (.with_char_limit(40) and
.with_desired_width(280.0)) with those constants; declare the constants near the
top of the module (or next to related UI constants) so intent and
maintainability are clearer while keeping the variable name
dashmate_password_input and the PasswordInput::new() builder usage unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cc7dadfa-c96e-4c36-a139-624753925242

📥 Commits

Reviewing files that changed from the base of the PR and between 5a508b3 and 9e5cf7a.

📒 Files selected for processing (4)
  • src/backend_task/core/mod.rs
  • src/backend_task/error.rs
  • src/ui/components/password_input.rs
  • src/ui/network_chooser_screen.rs

Comment thread src/backend_task/error.rs
@lklimek lklimek disabled auto-merge March 23, 2026 11:14
@lklimek lklimek merged commit 6752f2c into v1.0-dev Mar 23, 2026
10 of 11 checks passed
@lklimek lklimek deleted the fix/rpc-auth-401-detection branch March 23, 2026 11:14
Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The PR correctly detects 401 auth errors and surfaces them as actionable user-facing messages. However, early-returning an error from GetBestChainLocks bypasses the existing success path that drives the connection status indicator offline, causing a stale 'connected' indicator after credentials fail. The error message text and password field limit also need minor adjustments.

Reviewed commit: 9e5cf7a

🔴 1 blocking | 🟡 2 suggestion(s) | 💬 1 nitpick(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/backend_task/core/mod.rs`:
- [BLOCKING] lines 207-211: Auth error early-return leaves connection status indicator stale
  When the active network returns HTTP 401, `GetBestChainLocks` now early-returns `Err(TaskError::CoreRpcAuthFailed)`. This becomes `TaskResult::Error`, but `ConnectionStatus::handle_task_result()` (line 409–436) only processes `TaskResult::Success` — it never calls `set_rpc_online(false)` for errors. Before this PR, auth failures were flattened to `None` via `.ok().flatten()`, producing `ChainLocks(None, ...)` which correctly drove `update_from_chainlocks()` → `set_rpc_online(false)`.

Result: after credentials break, the user sees the error banner but the connection status indicator stays 'connected'.

One fix: instead of early-returning, stash the auth error, continue to produce the `ChainLocks(...)` success result (with `None` for the failed network so `rpc_online` updates correctly), and surface the auth error through a secondary mechanism (e.g., a new result variant that carries both the chain locks and the auth warning). Alternatively, teach `handle_task_result()` to set `rpc_online = false` when it sees `TaskResult::Error` originating from a chain-lock task.

In `src/backend_task/error.rs`:
- [SUGGESTION] line 60: Error message mentions only password but a 401 can also mean wrong username
  The `CoreRpcAuthFailed` display says "Check your RPC password in settings" but the fallback auth path uses `Auth::UserPass(core_rpc_user, core_rpc_password)`, so a wrong username produces the same HTTP 401. The message should mention both.

In `src/ui/network_chooser_screen.rs`:
- [SUGGESTION] line 126: 40-character password limit may truncate valid RPC passwords
  `.with_char_limit(40)` silently prevents entry of longer passwords. Dash Core's `rpcpassword` accepts arbitrary-length strings, and dashmate or password generators can produce 64+ character values. The field already has `with_desired_width(280.0)` for visual sizing, so the character limit can be raised safely.

Comment on lines +207 to +211
if let Err(e) = active_result
&& is_rpc_auth_error(e)
{
return Err(TaskError::CoreRpcAuthFailed);
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Auth error early-return leaves connection status indicator stale

When the active network returns HTTP 401, GetBestChainLocks now early-returns Err(TaskError::CoreRpcAuthFailed). This becomes TaskResult::Error, but ConnectionStatus::handle_task_result() (line 409–436) only processes TaskResult::Success — it never calls set_rpc_online(false) for errors. Before this PR, auth failures were flattened to None via .ok().flatten(), producing ChainLocks(None, ...) which correctly drove update_from_chainlocks()set_rpc_online(false).

Result: after credentials break, the user sees the error banner but the connection status indicator stays 'connected'.

One fix: instead of early-returning, stash the auth error, continue to produce the ChainLocks(...) success result (with None for the failed network so rpc_online updates correctly), and surface the auth error through a secondary mechanism (e.g., a new result variant that carries both the chain locks and the auth warning). Alternatively, teach handle_task_result() to set rpc_online = false when it sees TaskResult::Error originating from a chain-lock task.

source: ['codex-general']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/backend_task/core/mod.rs`:
- [BLOCKING] lines 207-211: Auth error early-return leaves connection status indicator stale
  When the active network returns HTTP 401, `GetBestChainLocks` now early-returns `Err(TaskError::CoreRpcAuthFailed)`. This becomes `TaskResult::Error`, but `ConnectionStatus::handle_task_result()` (line 409–436) only processes `TaskResult::Success` — it never calls `set_rpc_online(false)` for errors. Before this PR, auth failures were flattened to `None` via `.ok().flatten()`, producing `ChainLocks(None, ...)` which correctly drove `update_from_chainlocks()` → `set_rpc_online(false)`.

Result: after credentials break, the user sees the error banner but the connection status indicator stays 'connected'.

One fix: instead of early-returning, stash the auth error, continue to produce the `ChainLocks(...)` success result (with `None` for the failed network so `rpc_online` updates correctly), and surface the auth error through a secondary mechanism (e.g., a new result variant that carries both the chain locks and the auth warning). Alternatively, teach `handle_task_result()` to set `rpc_online = false` when it sees `TaskResult::Error` originating from a chain-lock task.

Comment thread src/backend_task/error.rs
CoreWalletNotConfigured,

/// Dash Core RPC rejected the request due to invalid credentials (HTTP 401).
#[error("Dash Core rejected your credentials. Check your RPC password in settings.")]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Error message mentions only password but a 401 can also mean wrong username

The CoreRpcAuthFailed display says "Check your RPC password in settings" but the fallback auth path uses Auth::UserPass(core_rpc_user, core_rpc_password), so a wrong username produces the same HTTP 401. The message should mention both.

💡 Suggested change
Suggested change
#[error("Dash Core rejected your credentials. Check your RPC password in settings.")]
#[error("Dash Core rejected your credentials. Check your RPC username and password in settings.")]

source: ['codex-general', 'claude-general']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/backend_task/error.rs`:
- [SUGGESTION] line 60: Error message mentions only password but a 401 can also mean wrong username
  The `CoreRpcAuthFailed` display says "Check your RPC password in settings" but the fallback auth path uses `Auth::UserPass(core_rpc_user, core_rpc_password)`, so a wrong username produces the same HTTP 401. The message should mention both.

let mut dashmate_password_input = PasswordInput::new().with_hint_text("Core RPC password");
let mut dashmate_password_input = PasswordInput::new()
.with_hint_text("Core RPC password")
.with_char_limit(40)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: 40-character password limit may truncate valid RPC passwords

.with_char_limit(40) silently prevents entry of longer passwords. Dash Core's rpcpassword accepts arbitrary-length strings, and dashmate or password generators can produce 64+ character values. The field already has with_desired_width(280.0) for visual sizing, so the character limit can be raised safely.

💡 Suggested change
Suggested change
.with_char_limit(40)
.with_char_limit(128)

source: ['claude-rust-quality', 'claude-general']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/ui/network_chooser_screen.rs`:
- [SUGGESTION] line 126: 40-character password limit may truncate valid RPC passwords
  `.with_char_limit(40)` silently prevents entry of longer passwords. Dash Core's `rpcpassword` accepts arbitrary-length strings, and dashmate or password generators can produce 64+ character values. The field already has `with_desired_width(280.0)` for visual sizing, so the character limit can be raised safely.

Network::Testnet => &testnet_result,
Network::Devnet => &devnet_result,
Network::Regtest => &local_result,
_ => &mainnet_result,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: Wildcard arm silently maps unknown Network variants to mainnet

Network is #[non_exhaustive], so the _ => &mainnet_result fallback is required. But if a new variant is added, this silently treats its auth check as mainnet's. A tracing::warn! in the wildcard arm would surface the gap.

source: ['claude-rust-quality']

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.

3 participants