diff --git a/src/rpc/xmlrpc_tinyxml2.cc b/src/rpc/xmlrpc_tinyxml2.cc
index 083da9ae..681c5b8c 100644
--- a/src/rpc/xmlrpc_tinyxml2.cc
+++ b/src/rpc/xmlrpc_tinyxml2.cc
@@ -326,15 +326,20 @@ process_document(const tinyxml2::XMLDocument* doc, tinyxml2::XMLPrinter* printer
auto& result_list = result.as_list();
auto parent_elements = element_access(doc->RootElement(), {"params", "param", "value", "array", "data"});
for (auto child = parent_elements->FirstChildElement("value"); child; child = child->NextSiblingElement("value")) {
- auto sub_method_name = element_access(child, {"struct", "member", "value", "string"})->GetText();
+ auto method_name_member = element_access(child, {"struct", "member"});
+ auto member_name = method_name_member->FirstChildElement("name");
+
+ if (member_name == nullptr || member_name->GetText() == nullptr ||
+ std::strncmp(member_name->GetText(), "methodName", sizeof("methodName")) != 0)
+ throw rpc_error(XMLRPC_PARSE_ERROR, "multicall struct's first member must be methodName");
+
+ auto sub_method_name = element_access(method_name_member, {"value", "string"})->GetText();
if (sub_method_name == nullptr)
throw rpc_error(XMLRPC_PARSE_ERROR, "multicall methodName element is empty");
// If sub_params ends up a nullptr at the end of this if-chian,
// execute_command will turn it into an empty list
- auto sub_params = element_access(child, {"struct", "member"});
- if (sub_params != nullptr)
- sub_params = sub_params->NextSiblingElement("member");
+ auto sub_params = method_name_member->NextSiblingElement("member");
if (sub_params != nullptr)
sub_params = sub_params->FirstChildElement("value");
if (sub_params != nullptr)
diff --git a/test/rpc/test_xmlrpc.cc b/test/rpc/test_xmlrpc.cc
index b3d68ead..44706fb9 100644
--- a/test/rpc/test_xmlrpc.cc
+++ b/test/rpc/test_xmlrpc.cc
@@ -192,12 +192,54 @@ TestXmlrpc::test_response_size_limit() {
CPPUNIT_ASSERT_EQUAL(expected, output);
}
+namespace {
+
+const std::string multicall_method_name =
+ "methodNamexmlrpc_reflect";
+const std::string multicall_params =
+ "params"
+ "a"
+ "";
+
+std::string
+multicall_request(const std::string& members) {
+ return "system.multicall"
+ "" + members +
+ "";
+}
+
+}
+
+void
+TestXmlrpc::test_multicall_member_order() {
+ auto call = [this](const std::string& input) {
+ std::string output;
+ m_xmlrpc.process(input.c_str(), input.size(), [&output](const char* c, uint32_t l){ output.append(c, l); return true;});
+ return output;
+ };
+
+ // methodName first is the only order accepted; the positive control proves
+ // the harness drives the real code path rather than a stub.
+ std::string ordered = call(multicall_request(multicall_method_name + multicall_params));
+ CPPUNIT_ASSERT(ordered.find("faultCode") == std::string::npos);
+
+ // params before methodName is rejected with a clear top-level fault,
+ // instead of the "could not find expected element string" of a positional read.
+ std::string expected_fault =
+ ""
+ "faultCode-503"
+ "faultStringmulticall struct's first member must be methodName"
+ "";
+ CPPUNIT_ASSERT_EQUAL(expected_fault, call(multicall_request(multicall_params + multicall_method_name)));
+}
+
#else
void TestXmlrpc::test_invalid_utf8() {}
void TestXmlrpc::test_basics() {}
void TestXmlrpc::test_size_limit() {}
void TestXmlrpc::test_response_size_limit() {}
+void TestXmlrpc::test_multicall_member_order() {}
void TestXmlrpc::setUp() {}
void TestXmlrpc::tearDown() {}
diff --git a/test/rpc/test_xmlrpc.h b/test/rpc/test_xmlrpc.h
index 7e788b52..d41788e1 100644
--- a/test/rpc/test_xmlrpc.h
+++ b/test/rpc/test_xmlrpc.h
@@ -11,6 +11,7 @@ class TestXmlrpc : public test_fixture {
CPPUNIT_TEST(test_invalid_utf8);
CPPUNIT_TEST(test_size_limit);
CPPUNIT_TEST(test_response_size_limit);
+ CPPUNIT_TEST(test_multicall_member_order);
CPPUNIT_TEST_SUITE_END();
@@ -24,6 +25,7 @@ public:
void test_invalid_utf8();
void test_size_limit();
void test_response_size_limit();
+ void test_multicall_member_order();
private:
std::unique_ptr m_test_main_thread;