diff --git a/CMakeLists.txt b/CMakeLists.txt index 2033abc3..141890b1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -168,7 +168,7 @@ if(BUILD_TESTS) unittests/backend/cassandra/ExecutionStrategyTests.cpp unittests/backend/cassandra/AsyncExecutorTests.cpp unittests/webserver/ServerTest.cpp - unittests/webserver/RPCExecutorTest.cpp) + unittests/webserver/RPCServerHandlerTest.cpp) include(CMake/deps/gtest.cmake) # fix for dwarf5 bug on ci diff --git a/src/main/main.cpp b/src/main/main.cpp index a6169dd1..aff3c187 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -31,7 +31,7 @@ #include #include #include -#include +#include #include #include @@ -206,11 +206,11 @@ try config, backend, subscriptions, balancer, etl, dosGuard, workQueue, counters, handlerProvider); // init the web server - auto executor = - std::make_shared>(config, backend, rpcEngine, etl, subscriptions); + auto handler = + std::make_shared>(config, backend, rpcEngine, etl, subscriptions); auto ctx = parseCerts(config); auto const ctxRef = ctx ? std::optional>{ctx.value()} : std::nullopt; - auto const httpServer = Server::make_HttpServer(config, ioc, ctxRef, dosGuard, executor); + auto const httpServer = Server::make_HttpServer(config, ioc, ctxRef, dosGuard, handler); // Blocks until stopped. // When stopped, shared_ptrs fall out of scope diff --git a/src/rpc/Errors.cpp b/src/rpc/Errors.cpp index c376e7aa..5ca1f626 100644 --- a/src/rpc/Errors.cpp +++ b/src/rpc/Errors.cpp @@ -81,6 +81,10 @@ getErrorInfo(ClioError code) {ClioError::rpcINVALID_HOT_WALLET, "invalidHotWallet", "Invalid hot wallet."}, {ClioError::rpcUNKNOWN_OPTION, "unknownOption", "Unknown option."}, {ClioError::rpcINVALID_API_VERSION, JS(invalid_API_version), "Invalid API version."}, + {ClioError::rpcCOMMAND_IS_MISSING, JS(missingCommand), "Method is not specified or is not a string."}, + {ClioError::rpcCOMMAND_NOT_STRING, "commandNotString", "Method is not a string."}, + {ClioError::rpcCOMMAND_IS_EMPTY, "emptyCommand", "Method is an empty string."}, + {ClioError::rpcPARAMS_UNPARSEABLE, "paramsUnparseable", "Params must be an array holding exactly one object."}, }; auto matchByCode = [code](auto const& info) { return info.code == code; }; diff --git a/src/rpc/Errors.h b/src/rpc/Errors.h index 0302b774..8880dc0d 100644 --- a/src/rpc/Errors.h +++ b/src/rpc/Errors.h @@ -44,6 +44,10 @@ enum class ClioError { // special system errors start with 6000 rpcINVALID_API_VERSION = 6000, + rpcCOMMAND_IS_MISSING = 6001, + rpcCOMMAND_NOT_STRING = 6002, + rpcCOMMAND_IS_EMPTY = 6003, + rpcPARAMS_UNPARSEABLE = 6004, }; /** @@ -210,6 +214,15 @@ static Status OK; WarningInfo const& getWarningInfo(WarningCode code); +/** + * @brief Get the error info object from an clio-specific error code. + * + * @param code The error code + * @return ClioErrorInfo const& A reference to the static error info + */ +ClioErrorInfo const& +getErrorInfo(ClioError code); + /** * @brief Generate JSON from a warning code. * diff --git a/src/rpc/Factories.cpp b/src/rpc/Factories.cpp index 31095a45..ec403a4b 100644 --- a/src/rpc/Factories.cpp +++ b/src/rpc/Factories.cpp @@ -44,7 +44,7 @@ make_WsContext( commandValue = request.at("command"); if (!commandValue.is_string()) - return Error{{RippledError::rpcBAD_SYNTAX, "Method/Command is not specified or is not a string."}}; + return Error{{ClioError::rpcCOMMAND_IS_MISSING, "Method/Command is not specified or is not a string."}}; auto const apiVersion = apiVersionParser.get().parse(request); if (!apiVersion) @@ -65,21 +65,27 @@ make_HttpContext( { using Error = util::Unexpected; - if (!request.contains("method") || !request.at("method").is_string()) - return Error{{RippledError::rpcBAD_SYNTAX, "Method is not specified or is not a string."}}; + if (!request.contains("method")) + return Error{{ClioError::rpcCOMMAND_IS_MISSING}}; - string const& command = request.at("method").as_string().c_str(); + if (!request.at("method").is_string()) + return Error{{ClioError::rpcCOMMAND_NOT_STRING}}; + + if (request.at("method").as_string().empty()) + return Error{{ClioError::rpcCOMMAND_IS_EMPTY}}; + + string command = request.at("method").as_string().c_str(); if (command == "subscribe" || command == "unsubscribe") return Error{{RippledError::rpcBAD_SYNTAX, "Subscribe and unsubscribe are only allowed or websocket."}}; if (!request.at("params").is_array()) - return Error{{RippledError::rpcBAD_SYNTAX, "Missing params array."}}; + return Error{{ClioError::rpcPARAMS_UNPARSEABLE, "Missing params array."}}; boost::json::array const& array = request.at("params").as_array(); if (array.size() != 1 || !array.at(0).is_object()) - return Error{{RippledError::rpcBAD_SYNTAX, "Params must be an array holding exactly one object."}}; + return Error{{ClioError::rpcPARAMS_UNPARSEABLE}}; auto const apiVersion = apiVersionParser.get().parse(request.at("params").as_array().at(0).as_object()); if (!apiVersion) diff --git a/src/webserver/RPCExecutor.h b/src/webserver/RPCServerHandler.h similarity index 84% rename from src/webserver/RPCExecutor.h rename to src/webserver/RPCServerHandler.h index 8872f113..59d55706 100644 --- a/src/webserver/RPCExecutor.h +++ b/src/webserver/RPCServerHandler.h @@ -25,14 +25,17 @@ #include #include #include +#include #include /** - * @brief The executor for RPC requests called by web server + * @brief The server handler for RPC requests called by web server + * + * Note: see ServerHandler concept */ template -class RPCExecutor +class RPCServerHandler { std::shared_ptr const backend_; std::shared_ptr const rpcEngine_; @@ -46,7 +49,7 @@ class RPCExecutor clio::Logger perfLog_{"Performance"}; public: - RPCExecutor( + RPCServerHandler( clio::Config const& config, std::shared_ptr const& backend, std::shared_ptr const& rpcEngine, @@ -84,18 +87,20 @@ public: connection->clientIp)) { rpcEngine_->notifyTooBusy(); - connection->send( - boost::json::serialize(RPC::makeError(RPC::RippledError::rpcTOO_BUSY)), - boost::beast::http::status::ok); + Server::detail::ErrorHelper(connection).sendTooBusyError(); } } - catch (boost::system::system_error const&) + catch (boost::system::system_error const& ex) { // system_error thrown when json parsing failed rpcEngine_->notifyBadSyntax(); - connection->send( - boost::json::serialize(RPC::makeError(RPC::RippledError::rpcBAD_SYNTAX)), - boost::beast::http::status::ok); + Server::detail::ErrorHelper(connection).sendJsonParsingError(ex.what()); + } + catch (std::invalid_argument const& ex) + { + // thrown when json parses something that is not an object at top level + rpcEngine_->notifyBadSyntax(); + Server::detail::ErrorHelper(connection).sendJsonParsingError(ex.what()); } catch (std::exception const& ex) { @@ -129,20 +134,6 @@ private: << " received request from work queue: " << util::removeSecret(request) << " ip = " << connection->clientIp; - auto const id = request.contains("id") ? request.at("id") : nullptr; - - auto const composeError = [&](auto const& error) -> boost::json::object { - auto e = RPC::makeError(error); - if (!id.is_null()) - e["id"] = id; - e["request"] = request; - - if (connection->upgraded) - return e; - else - return {{"result", e}}; - }; - try { auto const range = backend_->fetchLedgerRange(); @@ -150,9 +141,7 @@ private: { // for error that happened before the handler, we don't attach any warnings rpcEngine_->notifyNotReady(); - return connection->send( - boost::json::serialize(composeError(RPC::RippledError::rpcNOT_READY)), - boost::beast::http::status::ok); + return Server::detail::ErrorHelper(connection, request).sendNotReadyError(); } auto const context = [&] { @@ -181,8 +170,10 @@ private: perfLog_.warn() << connection->tag() << "Could not create Web context: " << err; log_.warn() << connection->tag() << "Could not create Web context: " << err; + // we count all those as BadSyntax - as the WS path would. + // Although over HTTP these will yield a 400 status with a plain text response (for most). rpcEngine_->notifyBadSyntax(); - return connection->send(boost::json::serialize(composeError(err)), boost::beast::http::status::ok); + return Server::detail::ErrorHelper(connection, request).sendError(err); } auto [v, timeDiff] = util::timed([&]() { return rpcEngine_->buildResponse(*context); }); @@ -194,7 +185,7 @@ private: if (auto const status = std::get_if(&v)) { // note: error statuses are counted/notified in buildResponse itself - response = std::move(composeError(*status)); + response = Server::detail::ErrorHelper(connection, request).composeError(*status); auto const responseStr = boost::json::serialize(response); perfLog_.debug() << context->tag() << "Encountered error: " << responseStr; @@ -225,10 +216,14 @@ private: // otherwise the "status" is in the "result" field if (connection->upgraded) { - if (!id.is_null()) + auto const id = request.contains("id") ? request.at("id") : nullptr; + + if (not id.is_null()) response["id"] = id; + if (!response.contains("error")) response["status"] = "success"; + response["type"] = "response"; } else @@ -245,7 +240,7 @@ private: warnings.emplace_back(RPC::makeWarning(RPC::warnRPC_OUTDATED)); response["warnings"] = warnings; - connection->send(boost::json::serialize(response), boost::beast::http::status::ok); + connection->send(boost::json::serialize(response)); } catch (std::exception const& ex) { @@ -255,10 +250,7 @@ private: log_.error() << connection->tag() << "Caught exception: " << ex.what(); rpcEngine_->notifyInternalError(); - - return connection->send( - boost::json::serialize(composeError(RPC::RippledError::rpcINTERNAL)), - boost::beast::http::status::internal_server_error); + return Server::detail::ErrorHelper(connection, request).sendInternalError(); } } }; diff --git a/src/webserver/details/ErrorHandling.h b/src/webserver/details/ErrorHandling.h new file mode 100644 index 00000000..0f8964c0 --- /dev/null +++ b/src/webserver/details/ErrorHandling.h @@ -0,0 +1,160 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include +#include + +#include +#include + +#include +#include +#include + +namespace Server::detail { + +/** + * @brief A helper that attempts to match rippled reporting mode HTTP errors as close as possible. + */ +class ErrorHelper +{ + std::shared_ptr connection_; + std::optional request_; + +public: + ErrorHelper( + std::shared_ptr const& connection, + std::optional request = std::nullopt) + : connection_{connection}, request_{std::move(request)} + { + } + + void + sendError(RPC::Status const& err) const + { + if (connection_->upgraded) + { + connection_->send(boost::json::serialize(composeError(err))); + } + else + { + // Note: a collection of crutches to match rippled output follows + if (auto const clioCode = std::get_if(&err.code)) + { + switch (*clioCode) + { + case RPC::ClioError::rpcINVALID_API_VERSION: + connection_->send( + std::string{RPC::getErrorInfo(*clioCode).error}, boost::beast::http::status::bad_request); + break; + case RPC::ClioError::rpcCOMMAND_IS_MISSING: + connection_->send("Null method", boost::beast::http::status::bad_request); + break; + case RPC::ClioError::rpcCOMMAND_IS_EMPTY: + connection_->send("method is empty", boost::beast::http::status::bad_request); + break; + case RPC::ClioError::rpcCOMMAND_NOT_STRING: + connection_->send("method is not string", boost::beast::http::status::bad_request); + break; + case RPC::ClioError::rpcPARAMS_UNPARSEABLE: + connection_->send("params unparseable", boost::beast::http::status::bad_request); + break; + + // others are not applicable but we want a compilation error next time we add one + case RPC::ClioError::rpcUNKNOWN_OPTION: + case RPC::ClioError::rpcMALFORMED_CURRENCY: + case RPC::ClioError::rpcMALFORMED_REQUEST: + case RPC::ClioError::rpcMALFORMED_OWNER: + case RPC::ClioError::rpcMALFORMED_ADDRESS: + case RPC::ClioError::rpcINVALID_HOT_WALLET: + assert(false); // this should never happen + break; + } + } + else + { + connection_->send(boost::json::serialize(composeError(err)), boost::beast::http::status::bad_request); + } + } + } + + void + sendInternalError() const + { + connection_->send( + boost::json::serialize(composeError(RPC::RippledError::rpcINTERNAL)), + boost::beast::http::status::internal_server_error); + } + + void + sendNotReadyError() const + { + connection_->send( + boost::json::serialize(composeError(RPC::RippledError::rpcNOT_READY)), boost::beast::http::status::ok); + } + + void + sendTooBusyError() const + { + if (connection_->upgraded) + connection_->send( + boost::json::serialize(RPC::makeError(RPC::RippledError::rpcTOO_BUSY)), boost::beast::http::status::ok); + else + connection_->send( + boost::json::serialize(RPC::makeError(RPC::RippledError::rpcTOO_BUSY)), + boost::beast::http::status::service_unavailable); + } + + void + sendJsonParsingError(std::string_view reason) const + { + if (connection_->upgraded) + connection_->send( + boost::json::serialize(RPC::makeError(RPC::RippledError::rpcBAD_SYNTAX)), + boost::beast::http::status::ok); + else + connection_->send( + fmt::format("Unable to parse request: {}", reason), boost::beast::http::status::bad_request); + } + + boost::json::object + composeError(auto const& error) const + { + auto e = RPC::makeError(error); + + if (request_) + { + auto const& req = request_.value(); + auto const id = req.contains("id") ? req.at("id") : nullptr; + if (not id.is_null()) + e["id"] = id; + + e["request"] = req; + } + + if (connection_->upgraded) + return e; + else + return {{"result", e}}; + } +}; + +} // namespace Server::detail diff --git a/src/webserver/details/WsBase.h b/src/webserver/details/WsBase.h index 742229db..71382054 100644 --- a/src/webserver/details/WsBase.h +++ b/src/webserver/details/WsBase.h @@ -223,6 +223,7 @@ public: auto sendError = [this](auto error, std::string&& requestStr) { auto e = RPC::makeError(error); + try { auto request = boost::json::parse(requestStr); @@ -235,29 +236,26 @@ public: e["request"] = std::move(requestStr); } - auto responseStr = boost::json::serialize(e); - log_.trace() << responseStr; - auto sharedMsg = std::make_shared(std::move(responseStr)); - send(std::move(sharedMsg)); + this->send(std::make_shared(boost::json::serialize(e))); }; - std::string msg{static_cast(buffer_.data().data()), buffer_.size()}; + std::string requestStr{static_cast(buffer_.data().data()), buffer_.size()}; // dosGuard served request++ and check ip address if (!dosGuard_.get().request(clientIp)) { // TODO: could be useful to count in counters in the future too - sendError(RPC::RippledError::rpcSLOW_DOWN, std::move(msg)); + sendError(RPC::RippledError::rpcSLOW_DOWN, std::move(requestStr)); } else { try { - (*handler_)(msg, shared_from_this()); + (*handler_)(requestStr, shared_from_this()); } catch (std::exception const&) { - sendError(RPC::RippledError::rpcINTERNAL, std::move(msg)); + sendError(RPC::RippledError::rpcINTERNAL, std::move(requestStr)); } } diff --git a/unittests/webserver/RPCExecutorTest.cpp b/unittests/webserver/RPCServerHandlerTest.cpp similarity index 83% rename from unittests/webserver/RPCExecutorTest.cpp rename to unittests/webserver/RPCServerHandlerTest.cpp index adad3f0a..58b2d51c 100644 --- a/unittests/webserver/RPCExecutorTest.cpp +++ b/unittests/webserver/RPCServerHandlerTest.cpp @@ -19,7 +19,7 @@ #include #include #include -#include +#include #include #include @@ -32,17 +32,20 @@ constexpr static auto MAXSEQ = 30; struct MockWsBase : public Server::ConnectionBase { std::string message; + boost::beast::http::status lastStatus = boost::beast::http::status::unknown; void send(std::shared_ptr msg_type) override { message += std::string(msg_type->data()); + lastStatus = boost::beast::http::status::ok; } void send(std::string&& msg, boost::beast::http::status status = boost::beast::http::status::ok) override { message += std::string(msg.data()); + lastStatus = status; } MockWsBase(util::TagDecoratorFactory const& factory) : Server::ConnectionBase(factory, "localhost.fake.ip") @@ -50,7 +53,7 @@ struct MockWsBase : public Server::ConnectionBase } }; -class WebRPCExecutorTest : public MockBackendTest +class WebRPCServerHandlerTest : public MockBackendTest { protected: void @@ -63,7 +66,7 @@ protected: tagFactory = std::make_shared(cfg); subManager = std::make_shared(cfg, mockBackendPtr); session = std::make_shared(*tagFactory); - rpcExecutor = std::make_shared>( + handler = std::make_shared>( cfg, mockBackendPtr, rpcEngine, etl, subManager); } @@ -77,12 +80,12 @@ protected: std::shared_ptr etl; std::shared_ptr subManager; std::shared_ptr tagFactory; - std::shared_ptr> rpcExecutor; + std::shared_ptr> handler; std::shared_ptr session; clio::Config cfg; }; -TEST_F(WebRPCExecutorTest, HTTPDefaultPath) +TEST_F(WebRPCServerHandlerTest, HTTPDefaultPath) { static auto constexpr request = R"({ "method": "server_info", @@ -110,12 +113,12 @@ TEST_F(WebRPCExecutorTest, HTTPDefaultPath) EXPECT_CALL(*etl, lastCloseAgeSeconds()).WillOnce(testing::Return(45)); - (*rpcExecutor)(std::move(request), session); + (*handler)(std::move(request), session); std::this_thread::sleep_for(200ms); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response)); } -TEST_F(WebRPCExecutorTest, WsNormalPath) +TEST_F(WebRPCServerHandlerTest, WsNormalPath) { session->upgraded = true; static auto constexpr request = R"({ @@ -145,12 +148,12 @@ TEST_F(WebRPCExecutorTest, WsNormalPath) EXPECT_CALL(*etl, lastCloseAgeSeconds()).WillOnce(testing::Return(45)); - (*rpcExecutor)(std::move(request), session); + (*handler)(std::move(request), session); std::this_thread::sleep_for(200ms); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response)); } -TEST_F(WebRPCExecutorTest, HTTPForwardedPath) +TEST_F(WebRPCServerHandlerTest, HTTPForwardedPath) { static auto constexpr request = R"({ "method": "server_info", @@ -185,12 +188,12 @@ TEST_F(WebRPCExecutorTest, HTTPForwardedPath) EXPECT_CALL(*etl, lastCloseAgeSeconds()).WillOnce(testing::Return(45)); - (*rpcExecutor)(std::move(request), session); + (*handler)(std::move(request), session); std::this_thread::sleep_for(200ms); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response)); } -TEST_F(WebRPCExecutorTest, WsForwardedPath) +TEST_F(WebRPCServerHandlerTest, WsForwardedPath) { session->upgraded = true; static auto constexpr request = R"({ @@ -228,12 +231,12 @@ TEST_F(WebRPCExecutorTest, WsForwardedPath) EXPECT_CALL(*etl, lastCloseAgeSeconds()).WillOnce(testing::Return(45)); - (*rpcExecutor)(std::move(request), session); + (*handler)(std::move(request), session); std::this_thread::sleep_for(200ms); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response)); } -TEST_F(WebRPCExecutorTest, HTTPErrorPath) +TEST_F(WebRPCServerHandlerTest, HTTPErrorPath) { static auto constexpr response = R"({ "result": { @@ -275,12 +278,12 @@ TEST_F(WebRPCExecutorTest, HTTPErrorPath) EXPECT_CALL(*etl, lastCloseAgeSeconds()).WillOnce(testing::Return(45)); - (*rpcExecutor)(std::move(requestJSON), session); + (*handler)(std::move(requestJSON), session); std::this_thread::sleep_for(200ms); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response)); } -TEST_F(WebRPCExecutorTest, WsErrorPath) +TEST_F(WebRPCServerHandlerTest, WsErrorPath) { session->upgraded = true; static auto constexpr response = R"({ @@ -316,12 +319,12 @@ TEST_F(WebRPCExecutorTest, WsErrorPath) EXPECT_CALL(*etl, lastCloseAgeSeconds()).WillOnce(testing::Return(45)); - (*rpcExecutor)(std::move(requestJSON), session); + (*handler)(std::move(requestJSON), session); std::this_thread::sleep_for(200ms); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response)); } -TEST_F(WebRPCExecutorTest, HTTPNotReady) +TEST_F(WebRPCServerHandlerTest, HTTPNotReady) { static auto constexpr request = R"({ "method": "server_info", @@ -344,12 +347,12 @@ TEST_F(WebRPCExecutorTest, HTTPNotReady) EXPECT_CALL(*rpcEngine, notifyNotReady).Times(1); - (*rpcExecutor)(std::move(request), session); + (*handler)(std::move(request), session); std::this_thread::sleep_for(200ms); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response)); } -TEST_F(WebRPCExecutorTest, WsNotReady) +TEST_F(WebRPCServerHandlerTest, WsNotReady) { session->upgraded = true; @@ -373,12 +376,12 @@ TEST_F(WebRPCExecutorTest, WsNotReady) EXPECT_CALL(*rpcEngine, notifyNotReady).Times(1); - (*rpcExecutor)(std::move(request), session); + (*handler)(std::move(request), session); std::this_thread::sleep_for(200ms); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response)); } -TEST_F(WebRPCExecutorTest, HTTPInvalidAPIVersion) +TEST_F(WebRPCServerHandlerTest, HTTPInvalidAPIVersion) { static auto constexpr request = R"({ "method": "server_info", @@ -390,30 +393,17 @@ TEST_F(WebRPCExecutorTest, HTTPInvalidAPIVersion) mockBackendPtr->updateRange(MINSEQ); // min mockBackendPtr->updateRange(MAXSEQ); // max - static auto constexpr response = R"({ - "result": { - "error": "invalid_API_version", - "error_code": 6000, - "error_message": "API version must be an integer", - "status": "error", - "type": "response", - "request": { - "method": "server_info", - "params": [{ - "api_version": null - }] - } - } - })"; + static auto constexpr response = "invalid_API_version"; EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1); - (*rpcExecutor)(std::move(request), session); + (*handler)(std::move(request), session); std::this_thread::sleep_for(200ms); - EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response)); + EXPECT_EQ(session->message, response); + EXPECT_EQ(session->lastStatus, boost::beast::http::status::bad_request); } -TEST_F(WebRPCExecutorTest, WSInvalidAPIVersion) +TEST_F(WebRPCServerHandlerTest, WSInvalidAPIVersion) { session->upgraded = true; static auto constexpr request = R"({ @@ -438,40 +428,12 @@ TEST_F(WebRPCExecutorTest, WSInvalidAPIVersion) EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1); - (*rpcExecutor)(std::move(request), session); + (*handler)(std::move(request), session); std::this_thread::sleep_for(200ms); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response)); } -TEST_F(WebRPCExecutorTest, HTTPBadSyntax) -{ - static auto constexpr request = R"({"method2": "server_info"})"; - - mockBackendPtr->updateRange(MINSEQ); // min - mockBackendPtr->updateRange(MAXSEQ); // max - - static auto constexpr response = R"({ - "result":{ - "error": "badSyntax", - "error_code": 1, - "error_message": "Method is not specified or is not a string.", - "status": "error", - "type": "response", - "request": { - "method2": "server_info", - "params": [{}] - } - } - })"; - - EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1); - - (*rpcExecutor)(std::move(request), session); - std::this_thread::sleep_for(200ms); - EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response)); -} - -TEST_F(WebRPCExecutorTest, HTTPBadSyntaxWhenRequestSubscribe) +TEST_F(WebRPCServerHandlerTest, HTTPBadSyntaxWhenRequestSubscribe) { static auto constexpr request = R"({"method": "subscribe"})"; @@ -494,12 +456,63 @@ TEST_F(WebRPCExecutorTest, HTTPBadSyntaxWhenRequestSubscribe) EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1); - (*rpcExecutor)(std::move(request), session); + (*handler)(std::move(request), session); std::this_thread::sleep_for(200ms); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response)); } -TEST_F(WebRPCExecutorTest, WsBadSyntax) +TEST_F(WebRPCServerHandlerTest, HTTPMissingCommand) +{ + static auto constexpr request = R"({"method2": "server_info"})"; + + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + + static auto constexpr response = "Null method"; + + EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1); + + (*handler)(std::move(request), session); + std::this_thread::sleep_for(200ms); + EXPECT_EQ(session->message, response); + EXPECT_EQ(session->lastStatus, boost::beast::http::status::bad_request); +} + +TEST_F(WebRPCServerHandlerTest, HTTPCommandNotString) +{ + static auto constexpr request = R"({"method": 1})"; + + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + + static auto constexpr response = "method is not string"; + + EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1); + + (*handler)(std::move(request), session); + std::this_thread::sleep_for(200ms); + EXPECT_EQ(session->message, response); + EXPECT_EQ(session->lastStatus, boost::beast::http::status::bad_request); +} + +TEST_F(WebRPCServerHandlerTest, HTTPCommandIsEmpty) +{ + static auto constexpr request = R"({"method": ""})"; + + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + + static auto constexpr response = "method is empty"; + + EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1); + + (*handler)(std::move(request), session); + std::this_thread::sleep_for(200ms); + EXPECT_EQ(session->message, response); + EXPECT_EQ(session->lastStatus, boost::beast::http::status::bad_request); +} + +TEST_F(WebRPCServerHandlerTest, WsMissingCommand) { session->upgraded = true; static auto constexpr request = R"({ @@ -511,8 +524,8 @@ TEST_F(WebRPCExecutorTest, WsBadSyntax) mockBackendPtr->updateRange(MAXSEQ); // max static auto constexpr response = R"({ - "error": "badSyntax", - "error_code": 1, + "error": "missingCommand", + "error_code": 6001, "error_message": "Method/Command is not specified or is not a string.", "status": "error", "type": "response", @@ -525,12 +538,52 @@ TEST_F(WebRPCExecutorTest, WsBadSyntax) EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1); - (*rpcExecutor)(std::move(request), session); + (*handler)(std::move(request), session); std::this_thread::sleep_for(200ms); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response)); } -TEST_F(WebRPCExecutorTest, HTTPInternalError) +TEST_F(WebRPCServerHandlerTest, HTTPParamsUnparseableNotArray) +{ + static auto constexpr response = "params unparseable"; + + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + + static auto constexpr requestJSON = R"({ + "method": "ledger", + "params": "wrong" + })"; + + EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1); + + (*handler)(std::move(requestJSON), session); + std::this_thread::sleep_for(200ms); + EXPECT_EQ(session->message, response); + EXPECT_EQ(session->lastStatus, boost::beast::http::status::bad_request); +} + +TEST_F(WebRPCServerHandlerTest, HTTPParamsUnparseableEmptyArray) +{ + static auto constexpr response = "params unparseable"; + + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + + static auto constexpr requestJSON = R"({ + "method": "ledger", + "params": [] + })"; + + EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1); + + (*handler)(std::move(requestJSON), session); + std::this_thread::sleep_for(200ms); + EXPECT_EQ(session->message, response); + EXPECT_EQ(session->lastStatus, boost::beast::http::status::bad_request); +} + +TEST_F(WebRPCServerHandlerTest, HTTPInternalError) { static auto constexpr response = R"({ "result": { @@ -557,12 +610,12 @@ TEST_F(WebRPCExecutorTest, HTTPInternalError) EXPECT_CALL(*rpcEngine, notifyInternalError).Times(1); EXPECT_CALL(*rpcEngine, buildResponse(testing::_)).Times(1).WillOnce(testing::Throw(std::runtime_error("MyError"))); - (*rpcExecutor)(std::move(requestJSON), session); + (*handler)(std::move(requestJSON), session); std::this_thread::sleep_for(200ms); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response)); } -TEST_F(WebRPCExecutorTest, WsInternalError) +TEST_F(WebRPCServerHandlerTest, WsInternalError) { session->upgraded = true; @@ -590,12 +643,12 @@ TEST_F(WebRPCExecutorTest, WsInternalError) EXPECT_CALL(*rpcEngine, notifyInternalError).Times(1); EXPECT_CALL(*rpcEngine, buildResponse(testing::_)).Times(1).WillOnce(testing::Throw(std::runtime_error("MyError"))); - (*rpcExecutor)(std::move(requestJSON), session); + (*handler)(std::move(requestJSON), session); std::this_thread::sleep_for(200ms); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response)); } -TEST_F(WebRPCExecutorTest, HTTPOutDated) +TEST_F(WebRPCServerHandlerTest, HTTPOutDated) { static auto constexpr request = R"({ "method": "server_info", @@ -627,12 +680,12 @@ TEST_F(WebRPCExecutorTest, HTTPOutDated) EXPECT_CALL(*etl, lastCloseAgeSeconds()).WillOnce(testing::Return(61)); - (*rpcExecutor)(std::move(request), session); + (*handler)(std::move(request), session); std::this_thread::sleep_for(200ms); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response)); } -TEST_F(WebRPCExecutorTest, WsOutdated) +TEST_F(WebRPCServerHandlerTest, WsOutdated) { session->upgraded = true; @@ -667,17 +720,17 @@ TEST_F(WebRPCExecutorTest, WsOutdated) EXPECT_CALL(*etl, lastCloseAgeSeconds()).WillOnce(testing::Return(61)); - (*rpcExecutor)(std::move(request), session); + (*handler)(std::move(request), session); std::this_thread::sleep_for(200ms); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response)); } -TEST_F(WebRPCExecutorTest, WsTooBusy) +TEST_F(WebRPCServerHandlerTest, WsTooBusy) { session->upgraded = true; auto localRpcEngine = std::make_shared(); - auto localRpcExecutor = std::make_shared>( + auto localHandler = std::make_shared>( cfg, mockBackendPtr, localRpcEngine, etl, subManager); static auto constexpr request = R"({ "command": "server_info", @@ -699,14 +752,14 @@ TEST_F(WebRPCExecutorTest, WsTooBusy) EXPECT_CALL(*localRpcEngine, notifyTooBusy).Times(1); EXPECT_CALL(*localRpcEngine, post).WillOnce(testing::Return(false)); - (*localRpcExecutor)(std::move(request), session); + (*localHandler)(std::move(request), session); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response)); } -TEST_F(WebRPCExecutorTest, HTTPTooBusy) +TEST_F(WebRPCServerHandlerTest, HTTPTooBusy) { auto localRpcEngine = std::make_shared(); - auto localRpcExecutor = std::make_shared>( + auto localHandler = std::make_shared>( cfg, mockBackendPtr, localRpcEngine, etl, subManager); static auto constexpr request = R"({ "method": "server_info", @@ -728,29 +781,23 @@ TEST_F(WebRPCExecutorTest, HTTPTooBusy) EXPECT_CALL(*localRpcEngine, notifyTooBusy).Times(1); EXPECT_CALL(*localRpcEngine, post).WillOnce(testing::Return(false)); - (*localRpcExecutor)(std::move(request), session); + (*localHandler)(std::move(request), session); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response)); } -TEST_F(WebRPCExecutorTest, HTTPRequestNotJson) +TEST_F(WebRPCServerHandlerTest, HTTPRequestNotJson) { static auto constexpr request = "not json"; - static auto constexpr response = - R"({ - "error": "badSyntax", - "error_code": 1, - "error_message": "Syntax error.", - "status": "error", - "type": "response" - })"; + static auto constexpr response = "Unable to parse request: syntax error"; EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1); - (*rpcExecutor)(std::move(request), session); - EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response)); + (*handler)(std::move(request), session); + EXPECT_EQ(session->message, response); + EXPECT_EQ(session->lastStatus, boost::beast::http::status::bad_request); } -TEST_F(WebRPCExecutorTest, WsRequestNotJson) +TEST_F(WebRPCServerHandlerTest, WsRequestNotJson) { session->upgraded = true; static auto constexpr request = "not json"; @@ -765,6 +812,6 @@ TEST_F(WebRPCExecutorTest, WsRequestNotJson) EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1); - (*rpcExecutor)(std::move(request), session); + (*handler)(std::move(request), session); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response)); }