The current “catch-all” catch (const std::exception& e) in CRPCTable::help is problematic, because it could catch exceptions unrelated to passing the help string up.
Fix this by using a dedicated exception type.
The current “catch-all” catch (const std::exception& e) in CRPCTable::help is problematic, because it could catch exceptions unrelated to passing the help string up.
Fix this by using a dedicated exception type.
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/32660.
See the guideline for information on the review process.
If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.
Can be tested by adding a bug, like
 0diff --git a/src/rpc/signmessage.cpp b/src/rpc/signmessage.cpp
 1index 5597f8d237..d62893bf0c 100644
 2--- a/src/rpc/signmessage.cpp
 3+++ b/src/rpc/signmessage.cpp
 4@@ -19,7 +19,7 @@ static RPCHelpMan verifymessage()
 5     return RPCHelpMan{"verifymessage",
 6         "Verify a signed message.",
 7         {
 8-            {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The bitcoin address to use for the signature."},
 9+            {"addres\ns", RPCArg::Type::STR, RPCArg::Optional::NO, "The bitcoin address to use for the signature."},
10             {"signature", RPCArg::Type::STR, RPCArg::Optional::NO, "The signature provided by the signer in base 64 encoding (see signmessage)."},
11             {"message", RPCArg::Type::STR, RPCArg::Optional::NO, "The message that was signed."},
12         },
On master, the bug is hidden. On this pull, it is more obvious. For example, ./test/functional/rpc_help.py or the help RPC fail.
66@@ -67,6 +67,10 @@ class FillableSigningProvider;
67 class CScript;
68 struct Sections;
69 
70+struct HelpException : std::runtime_error {
71+    explicit HelpException(const std::string& msg) : std::runtime_error{msg} {}
72+};
nits:
final0struct HelpException final : std::runtime_error
1{
2    explicit HelpException(const std::string& msg) : std::runtime_error{msg} {}
3};
88     jreq.params = UniValue();
89 
90-    for (const std::pair<std::string, const CRPCCommand*>& command : vCommands)
91-    {
92-        const CRPCCommand *pcmd = command.second;
93+    for (const auto& [_, pcmd] : vCommands) {
👍, alternatively, if you think it’s clearer:
0    for (const auto& pcmd : vCommands | std::views::values) {
It doesn’t seem like this is catching the issue in #29136 (comment)? I tried making a similar change to a RPC in this PR and the test still passed.
Are you sure? What are the exact steps to reproduce?
Locally such a bug passes for me on current master:
 0$ git checkout bitcoin-core/master && git diff && cmakeb && ./bld-cmake/test/functional/rpc_help.py 
 1M	src/rpc/signmessage.cpp
 2HEAD is now at 88b22acc3d Merge bitcoin/bitcoin#32528: rpc: Round verificationprogress to 1 for a recent tip
 3
 4
 5diff --git a/src/rpc/signmessage.cpp b/src/rpc/signmessage.cpp
 6index 5597f8d237..18af6d110e 100644
 7--- a/src/rpc/signmessage.cpp
 8+++ b/src/rpc/signmessage.cpp
 9@@ -17,7 +17,7 @@
10 static RPCHelpMan verifymessage()
11 {
12     return RPCHelpMan{"verifymessage",
13-        "Verify a signed message.",
14+        "\nVerify a signed message.",
15         {
16             {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The bitcoin address to use for the signature."},
17             {"signature", RPCArg::Type::STR, RPCArg::Optional::NO, "The signature provided by the signer in base 64 encoding (see signmessage)."},
whereas on this pull, the same diff fails.
whereas on this pull, the same diff fails.
Work for me:
 0./build/test/functional/rpc_help.py                        
 12025-06-03T07:21:05.244000Z TestFramework (INFO): PRNG seed is: 8715551814237508387
 22025-06-03T07:21:05.244000Z TestFramework (INFO): Initializing test directory /var/folders/sq/z88fhjzj0b19ftsd2_bjrmjm0000gn/T/bitcoin_func_test_ks0xf8o9
 32025-06-03T07:21:06.291000Z TestFramework (ERROR): JSONRPC error
 4Traceback (most recent call last):
 5  File "/bitcoin/test/functional/test_framework/test_framework.py", line 189, in main
 6    self.run_test()
 7    ~~~~~~~~~~~~~^^
 8  File "/bitcoin/./build/test/functional/rpc_help.py", line 53, in run_test
 9    self.test_categories()
10    ~~~~~~~~~~~~~~~~~~~~^^
11  File "/bitcoin/./build/test/functional/rpc_help.py", line 102, in test_categories
12    titles = [line[3:-3] for line in node.help().splitlines() if line.startswith('==')]
13                                     ~~~~~~~~~^^
14  File "/bitcoin/test/functional/test_framework/coverage.py", line 50, in __call__
15    return_val = self.auth_service_proxy_instance.__call__(*args, **kwargs)
16  File "/bitcoin/test/functional/test_framework/authproxy.py", line 151, in __call__
17    raise JSONRPCException(response['error'], status)
18test_framework.authproxy.JSONRPCException: Internal bug detected: !m_description.starts_with('\n')
19../../src/rpc/util.cpp:796 (ToString)
20Bitcoin Core v29.99.0-6497cef9bdc8-dirty
21Please report this issue here: https://github.com/bitcoin/bitcoin/issues
22 (-1)
232025-06-03T07:21:06.343000Z TestFramework (INFO): Not stopping nodes as test failed. The dangling processes will be cleaned up later.
0./build/bin/bitcoin-cli help    
1error code: -1
2error message:
3Internal bug detected: !m_description.starts_with('\n')
4../../src/rpc/util.cpp:796 (ToString)
5Bitcoin Core v29.99.0-6497cef9bdc8-dirty
6Please report this issue here: https://github.com/bitcoin/bitcoin/issues
644@@ -645,7 +645,7 @@ UniValue RPCHelpMan::HandleRequest(const JSONRPCRequest& request) const
645      * the user is asking for help information, and throw help when appropriate.
646      */
647     if (request.mode == JSONRPCRequest::GET_HELP || !IsValidNumArgs(request.params.size())) {
648-        throw std::runtime_error(ToString());
649+        throw HelpException{ToString()};
NonFatalCheckError exception is thrown from here that was earlier caught by the catch-all handler in server.cpp. Now only HelpException is caught and the rest, including NonFatalCheckError, is bubbled up all the way to the RPC response. https://github.com/bitcoin/bitcoin/blob/e872a566f251c73908de8b6d243c94a6679c2eac/src/rpc/util.cpp#L795-L796
              
            Nit: Maybe returning the result by throwing an exception is obvious to others but it wasn’t to me. Does it necessarily need to be called HelpException? Calling it HelpResult might make it more explicit or HelpResultException (not my preference).
 0diff --git a/src/rpc/server.cpp b/src/rpc/server.cpp
 1index d5c790e89b..bd99615bbf 100644
 2--- a/src/rpc/server.cpp
 3+++ b/src/rpc/server.cpp
 4@@ -96,7 +96,7 @@ std::string CRPCTable::help(const std::string& strCommand, const JSONRPCRequest&
 5             UniValue unused_result;
 6             if (setDone.insert(pcmd->unique_id).second)
 7                 pcmd->actor(jreq, unused_result, /*last_handler=*/true);
 8-        } catch (const HelpException& e) {
 9+        } catch (const HelpResult& e) {
10             std::string strHelp{e.what()};
11             if (strCommand == "")
12             {
13diff --git a/src/rpc/util.cpp b/src/rpc/util.cpp
14index 75b801f2bf..5da02b4df4 100644
15--- a/src/rpc/util.cpp
16+++ b/src/rpc/util.cpp
17@@ -645,7 +645,7 @@ UniValue RPCHelpMan::HandleRequest(const JSONRPCRequest& request) const
18      * the user is asking for help information, and throw help when appropriate.
19      */
20     if (request.mode == JSONRPCRequest::GET_HELP || !IsValidNumArgs(request.params.size())) {
21-        throw HelpException{ToString()};
22+        throw HelpResult{ToString()};
23     }
24     UniValue arg_mismatch{UniValue::VOBJ};
25     for (size_t i{0}; i < m_args.size(); ++i) {
26diff --git a/src/rpc/util.h b/src/rpc/util.h
27index 0a86768814..1650d2da1f 100644
28--- a/src/rpc/util.h
29+++ b/src/rpc/util.h
30@@ -67,8 +67,8 @@ class FillableSigningProvider;
31 class CScript;
32 struct Sections;
33 
34-struct HelpException : std::runtime_error {
35-    explicit HelpException(const std::string& msg) : std::runtime_error{msg} {}
36+struct HelpResult : std::runtime_error {
37+    explicit HelpResult(const std::string& msg) : std::runtime_error{msg} {}
38 };
HelpResult, if I have to re-touch for other reasons.
              
            git range-diff bitcoin-core/master fa0cf42380 fa946520d2.
              
            ACK fa0cf42380dec17e73d38aa69dd70662a0bc344a
 0➜  bitcoin git:(2506-rpc-help) ✗ git diff --color | cat
 1diff --git a/src/rpc/signmessage.cpp b/src/rpc/signmessage.cpp
 2index 5597f8d237..18af6d110e 100644
 3--- a/src/rpc/signmessage.cpp
 4+++ b/src/rpc/signmessage.cpp
 5@@ -17,7 +17,7 @@
 6 static RPCHelpMan verifymessage()
 7 {
 8     return RPCHelpMan{"verifymessage",
 9-        "Verify a signed message.",
10+        "\nVerify a signed message.",
11         {
12             {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The bitcoin address to use for the signature."},
13             {"signature", RPCArg::Type::STR, RPCArg::Optional::NO, "The signature provided by the signer in base 64 encoding (see signmessage)."},
 0➜  bitcoin git:(2506-rpc-help) ✗ bitcoinclireg help verifymessage  
 1error code: -1
 2error message:
 3Internal bug detected: !m_description.starts_with('\n')
 4rpc/util.cpp:796 (ToString)
 5Bitcoin Core v29.99.0-fa0cf42380de-dirty
 6Please report this issue here: https://github.com/bitcoin/bitcoin/issues
 7
 8➜  bitcoin git:(2506-rpc-help) ✗ bitcoinclireg help
 9error code: -1
10error message:
11Internal bug detected: !m_description.starts_with('\n')
12rpc/util.cpp:796 (ToString)
13Bitcoin Core v29.99.0-fa0cf42380de-dirty
14Please report this issue here: https://github.com/bitcoin/bitcoin/issues
 0➜  bitcoin git:(master) ✗ bitcoinclireg help verifymessage
 1Internal bug detected: !m_description.starts_with('\n')
 2rpc/util.cpp:796 (ToString)
 3Bitcoin Core v29.99.0-4b1d48a6866b-dirty
 4Please report this issue here: https://github.com/bitcoin/bitcoin/issues
 5
 6➜  bitcoin git:(master) ✗ bitcoinclireg help
 7...
 8...
 9signmessagewithprivkey "privkey" "message"
10validateaddress "address"
11Internal bug detected: !m_description.starts_with('\n')
12
13== Wallet ==
14abandontransaction "txid"
The rpc_help functional test also fails with the diff in the branch but not in master.
Are you sure? What are the exact steps to reproduce?
Nope.
Not sure what I did wrong yesterday, but tried again and it does seem to work.
I retested the scenario with "address\n;)", running
0cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug && ninja -C build && build/test/functional/test_runner.py  rpc_help
and got:
0Internal bug detected: s.m_left.find('\n') == std::string::npos
on master it’s:
0rpc_help.py | ✓ Passed
👍
tested ACK fa946520d229ae45b30519bccc9eaa2c47b4a093 (edited)
Reapplied the tests from #32660 (comment) - passes without the changes, fails with the change as expected!
git fetch origin fa0cf42380dec17e73d38aa69dd70662a0bc344a fa946520d229ae45b30519bccc9eaa2c47b4a093 git range-diff master fa946520d229ae45b30519bccc9eaa2c47b4a093 fa0cf42380dec17e73d38aa69dd70662a0bc344a
 01:  fa97f9b78e ! 1:  eeeec1579e rpc: Use type-safe HelpException
 1@@ Metadata
 2 Author: MarcoFalke <*~=`'#}+{/-|&$^_@721217.xyz>
 3 
 4## Commit message ##
 5-    rpc: Use type-safe HelpException
 6+    rpc: Use type-safe exception to pass RPC help
 7 
 8## src/rpc/server.cpp ##
 9@@ src/rpc/server.cpp: std::string CRPCTable::help(const std::string& strCommand, const JSONRPCRequest&
10@@ src/rpc/server.cpp: std::string CRPCTable::help(const std::string& strCommand, c
11 -        {
12 -            // Help text is returned in an exception
13 -            std::string strHelp = std::string(e.what());
14-+        } catch (const HelpException& e) {
15++        } catch (const HelpResult& e) {
16 +            std::string strHelp{e.what()};
17              if (strCommand == "")
18              {
19@@ src/rpc/util.cpp: UniValue RPCHelpMan::HandleRequest(const JSONRPCRequest& reque
20       */
21      if (request.mode == JSONRPCRequest::GET_HELP || !IsValidNumArgs(request.params.size())) {
22 -        throw std::runtime_error(ToString());
23-+        throw HelpException{ToString()};
24++        throw HelpResult{ToString()};
25      }
26      UniValue arg_mismatch{UniValue::VOBJ};
27      for (size_t i{0}; i < m_args.size(); ++i) {
28@@ src/rpc/util.h: class FillableSigningProvider;
29  class CScript;
30  struct Sections;
31  
32-+struct HelpException : std::runtime_error {
33-+    explicit HelpException(const std::string& msg) : std::runtime_error{msg} {}
34++struct HelpResult : std::runtime_error {
35++    explicit HelpResult(const std::string& msg) : std::runtime_error{msg} {}
36 +};
37 +
38  /**
392:  fa0cf42380 = 2:  fa946520d2 refactor: Use structured binding for-loop
re-ACK fa946520d229ae45b30519bccc9eaa2c47b4a093
0git range-diff fa0cf42...fa94652
Thanks for accepting the naming suggestion.