1 // Copyright (c) 2019 Sorin Fetche
3 // Distributed under the Boost Software License, Version 1.0. (See accompanying
4 // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6 // PLEASE NOTE: This example requires the Boost 1.70 version of Asio and Beast, which at the time of this
9 // Example of a composed asynchronous operation which uses the LEAF library for error handling and reporting.
11 // Examples of running:
12 // - in one terminal (re)run: ./asio_beast_leaf_rpc_v3 0.0.0.0 8080
14 // curl localhost:8080 -v -d "sum 0 1 2 3"
15 // generating errors returned to the client:
16 // curl localhost:8080 -v -X DELETE -d ""
17 // curl localhost:8080 -v -d "mul 1 2x3"
18 // curl localhost:8080 -v -d "div 1 0"
19 // curl localhost:8080 -v -d "mod 1"
21 // Runs that showcase the error handling on the server side:
22 // - error starting the server:
23 // ./asio_beast_leaf_rpc_v3 0.0.0.0 80
24 // - error while running the server logic:
25 // ./asio_beast_leaf_rpc_v3 0.0.0.0 8080
26 // curl localhost:8080 -v -d "error-quit"
28 #include <boost/algorithm/string/replace.hpp>
29 #include <boost/asio/io_context.hpp>
30 #include <boost/asio/ip/tcp.hpp>
31 #include <boost/beast/core.hpp>
32 #include <boost/beast/http.hpp>
33 #include <boost/beast/version.hpp>
34 #include <boost/format.hpp>
35 #include <boost/leaf.hpp>
36 #include <boost/spirit/include/qi_numeric.hpp>
37 #include <boost/spirit/include/qi_parse.hpp>
44 namespace beast
= boost::beast
;
45 namespace http
= beast::http
;
46 namespace leaf
= boost::leaf
;
47 namespace net
= boost::asio
;
50 using error_code
= boost::system::error_code
;
53 // The operation being performed when an error occurs.
54 struct e_last_operation
{
55 std::string_view value
;
58 // The HTTP request type.
59 using request_t
= http::request
<http::string_body
>;
60 // The HTTP response type.
61 using response_t
= http::response
<http::string_body
>;
63 response_t
handle_request(request_t
&&request
);
65 // A composed asynchronous operation that implements a basic remote calculator over HTTP.
66 // It receives from the remote side commands such as:
70 // in the body of POST requests and sends back the result.
72 // Besides the calculator related commands, it also offer a special command:
73 // - `error_quit` that asks the server to simulate a server side error that leads to the connection being dropped
76 // From the error handling perspective there are three parts of the implementation:
77 // - the handling of an HTTP request and creating the response to send back
78 // (see handle_request)
79 // - the parsing and execution of the remote command we received as the body of an an HTTP POST request
80 // (see execute_command())
81 // - this composed asynchronous operation which calls them
84 // This example operation is based on:
85 // - https://github.com/boostorg/beast/blob/b02f59ff9126c5a17f816852efbbd0ed20305930/example/echo-op/echo_op.cpp
87 // https://github.com/boostorg/beast/blob/b02f59ff9126c5a17f816852efbbd0ed20305930/example/advanced/server/advanced_server.cpp
89 template <class AsyncStream
, typename ErrorContext
, typename CompletionToken
>
90 auto async_demo_rpc(AsyncStream
&stream
, ErrorContext
&error_context
, CompletionToken
&&token
) ->
91 typename
net::async_result
<typename
std::decay
<CompletionToken
>::type
, void(leaf::result
<void>)>::return_type
{
93 static_assert(beast::is_async_stream
<AsyncStream
>::value
, "AsyncStream requirements not met");
96 typename
net::async_completion
<CompletionToken
, void(leaf::result
<void>)>::completion_handler_type
;
97 using base_type
= beast::stable_async_base
<handler_type
, beast::executor_type
<AsyncStream
>>;
98 struct internal_op
: base_type
{
99 // This object must have a stable address
100 struct temporary_data
{
101 beast::flat_buffer buffer
;
102 std::optional
<http::request_parser
<request_t::body_type
>> parser
;
103 std::optional
<response_t
> response
;
106 AsyncStream
&m_stream
;
107 ErrorContext
&m_error_context
;
108 temporary_data
&m_data
;
109 bool m_write_and_quit
;
111 internal_op(AsyncStream
&stream
, ErrorContext
&error_context
, handler_type
&&handler
)
112 : base_type
{std::move(handler
), stream
.get_executor()}, m_stream
{stream
}, m_error_context
{error_context
},
113 m_data
{beast::allocate_stable
<temporary_data
>(*this)}, m_write_and_quit
{false} {
114 start_read_request();
117 void operator()(error_code ec
, std::size_t /*bytes_transferred*/ = 0) {
118 leaf::result
<bool> result_continue_execution
;
120 auto active_context
= activate_context(m_error_context
);
121 auto load
= leaf::on_error(e_last_operation
{m_data
.response
? "async_demo_rpc::continuation-write"
122 : "async_demo_rpc::continuation-read"});
123 if (ec
== http::error::end_of_stream
) {
124 // The remote side closed the connection.
125 result_continue_execution
= false;
127 result_continue_execution
= leaf::new_error(ec
);
129 result_continue_execution
= leaf::exception_to_result([&]() -> leaf::result
<bool> {
130 if (!m_data
.response
) {
131 // Process the request we received.
132 m_data
.response
= handle_request(std::move(m_data
.parser
->release()));
133 m_write_and_quit
= m_data
.response
->need_eof();
134 http::async_write(m_stream
, *m_data
.response
, std::move(*this));
138 // If getting here, we completed a write operation.
139 m_data
.response
.reset();
140 // And start reading a new message if not quitting
141 // (i.e. the message semantics of the last response we sent required an end of file)
142 if (!m_write_and_quit
) {
143 start_read_request();
147 // We didn't initiate any new async operation above, so we will not continue the execution.
151 // The activation object and load_last_operation need to be reset before calling the completion handler
152 // This is because, in general, the completion handler may be called directly or posted and if posted,
153 // it could execute in another thread. This means that regardless of how the handler gets to be actually
154 // called we must ensure that it is not called with the error context active.
155 // Note: An error context cannot be activated twice
157 if (!result_continue_execution
) {
158 // We don't continue the execution due to an error, calling the completion handler
159 this->complete_now(result_continue_execution
.error());
160 } else if( !*result_continue_execution
) {
161 // We don't continue the execution due to the flag not being set, calling the completion handler
162 this->complete_now(leaf::result
<void>{});
166 void start_read_request() {
167 m_data
.parser
.emplace();
168 m_data
.parser
->body_limit(1024);
169 http::async_read(m_stream
, m_data
.buffer
, *m_data
.parser
, std::move(*this));
173 auto initiation
= [](auto &&completion_handler
, AsyncStream
*stream
, ErrorContext
*error_context
) {
174 internal_op op
{*stream
, *error_context
, std::forward
<decltype(completion_handler
)>(completion_handler
)};
177 // We are in the "initiation" part of the async operation.
178 [[maybe_unused
]] auto load
= leaf::on_error(e_last_operation
{"async_demo_rpc::initiation"});
179 return net::async_initiate
<CompletionToken
, void(leaf::result
<void>)>(initiation
, token
, &stream
, &error_context
);
182 // The location of a int64 parse error.
183 // It refers the range of characters from which the parsing was done.
184 struct e_parse_int64_error
{
185 using location_base
= std::pair
<std::string_view
const, std::string_view::const_iterator
>;
186 struct location
: public location_base
{
187 using location_base::location_base
;
189 friend std::ostream
&operator<<(std::ostream
&os
, location
const &value
) {
190 auto const &sv
= value
.first
;
191 std::size_t pos
= std::distance(sv
.begin(), value
.second
);
193 os
<< "->\"" << sv
<< "\"";
194 } else if (pos
< sv
.size()) {
195 os
<< "\"" << sv
.substr(0, pos
) << "\"->\"" << sv
.substr(pos
) << "\"";
197 os
<< "\"" << sv
<< "\"<-";
206 // Parses an integer from a string_view.
207 leaf::result
<std::int64_t> parse_int64(std::string_view word
) {
208 auto const begin
= word
.begin();
209 auto const end
= word
.end();
210 std::int64_t value
= 0;
212 bool result
= boost::spirit::qi::parse(i
, end
, boost::spirit::long_long
, value
);
213 if (!result
|| i
!= end
) {
214 return leaf::new_error(e_parse_int64_error
{std::make_pair(word
, i
)});
219 // The command being executed while we get an error.
220 // It refers the range of characters from which the command was extracted.
222 std::string_view value
;
225 // The details about an incorrect number of arguments error
226 // Some commands may accept a variable number of arguments (e.g. greater than 1 would mean [2, SIZE_MAX]).
227 struct e_unexpected_arg_count
{
233 friend std::ostream
&operator<<(std::ostream
&os
, arg_info
const &value
) {
234 os
<< value
.count
<< " (required: ";
235 if (value
.min
== value
.max
) {
237 } else if (value
.max
< SIZE_MAX
) {
238 os
<< "[" << value
.min
<< ", " << value
.max
<< "]";
240 os
<< "[" << value
.min
<< ", MAX]";
250 // The HTTP status that should be returned in case we get into an error.
251 struct e_http_status
{
255 // Unexpected HTTP method.
256 struct e_unexpected_http_method
{
260 // The E-type that describes the `error_quit` command as an error condition.
261 struct e_error_quit
{
266 // Processes a remote command.
267 leaf::result
<std::string
> execute_command(std::string_view line
) {
268 // Split the command in words.
269 std::list
<std::string_view
> words
; // or std::deque<std::string_view> words;
271 char const *const ws
= "\t \r\n";
272 auto skip_ws
= [&](std::string_view
&line
) {
273 if (auto pos
= line
.find_first_not_of(ws
); pos
!= std::string_view::npos
) {
274 line
= line
.substr(pos
);
276 line
= std::string_view
{};
281 while (!line
.empty()) {
282 std::string_view word
;
283 if (auto pos
= line
.find_first_of(ws
); pos
!= std::string_view::npos
) {
284 word
= line
.substr(0, pos
);
285 line
= line
.substr(pos
+ 1);
288 line
= std::string_view
{};
292 words
.push_back(word
);
297 static char const *const help
= "Help:\n"
298 " error-quit Simulated error to end the session\n"
299 " sum <int64>* Addition\n"
300 " sub <int64>+ Substraction\n"
301 " mul <int64>* Multiplication\n"
302 " div <int64>+ Division\n"
303 " mod <int64> <int64> Remainder\n"
304 " <anything else> This message";
307 return std::string(help
);
310 auto command
= words
.front();
313 auto load_cmd
= leaf::on_error(e_command
{command
}, e_http_status
{http::status::bad_request
});
314 std::string response
;
316 if (command
== "error-quit") {
317 return leaf::new_error(e_error_quit
{});
318 } else if (command
== "sum") {
319 std::int64_t sum
= 0;
320 for (auto const &w
: words
) {
321 BOOST_LEAF_AUTO(i
, parse_int64(w
));
324 response
= std::to_string(sum
);
325 } else if (command
== "sub") {
326 if (words
.size() < 2) {
327 return leaf::new_error(e_unexpected_arg_count
{words
.size(), 2, SIZE_MAX
});
329 BOOST_LEAF_AUTO(sub
, parse_int64(words
.front()));
331 for (auto const &w
: words
) {
332 BOOST_LEAF_AUTO(i
, parse_int64(w
));
335 response
= std::to_string(sub
);
336 } else if (command
== "mul") {
337 std::int64_t mul
= 1;
338 for (auto const &w
: words
) {
339 BOOST_LEAF_AUTO(i
, parse_int64(w
));
342 response
= std::to_string(mul
);
343 } else if (command
== "div") {
344 if (words
.size() < 2) {
345 return leaf::new_error(e_unexpected_arg_count
{words
.size(), 2, SIZE_MAX
});
347 BOOST_LEAF_AUTO(div
, parse_int64(words
.front()));
349 for (auto const &w
: words
) {
350 BOOST_LEAF_AUTO(i
, parse_int64(w
));
352 // In some cases this command execution function might throw, not just return an error.
353 throw std::runtime_error
{"division by zero"};
357 response
= std::to_string(div
);
358 } else if (command
== "mod") {
359 if (words
.size() != 2) {
360 return leaf::new_error(e_unexpected_arg_count
{words
.size(), 2, 2});
362 BOOST_LEAF_AUTO(i1
, parse_int64(words
.front()));
364 BOOST_LEAF_AUTO(i2
, parse_int64(words
.front()));
367 // In some cases this command execution function might throw, not just return an error.
368 throw leaf::exception(std::runtime_error
{"division by zero"});
370 response
= std::to_string(i1
% i2
);
378 std::string
diagnostic_to_str(leaf::verbose_diagnostic_info
const &diag
) {
379 auto str
= boost::str(boost::format("%1%") % diag
);
380 boost::algorithm::replace_all(str
, "\n", "\n ");
381 return "\nDetailed error diagnostic:\n----\n" + str
+ "\n----";
384 // Handles an HTTP request and returns the response to send back.
385 response_t
handle_request(request_t
&&request
) {
387 auto msg_prefix
= [](e_command
const *cmd
) {
388 if (cmd
!= nullptr) {
389 return boost::str(boost::format("Error (%1%):") % cmd
->value
);
391 return std::string("Error:");
394 auto make_sr
= [](e_http_status
const *status
, std::string
&&response
) {
395 return std::make_pair(status
!= nullptr ? status
->value
: http::status::internal_server_error
,
396 std::move(response
));
399 // In this variant of the RPC example we execute the remote command and handle any errors coming from it
400 // in one place (using `leaf::try_handle_all`).
401 auto pair_status_response
= leaf::try_handle_all(
402 [&]() -> leaf::result
<std::pair
<http::status
, std::string
>> {
403 if (request
.method() != http::verb::post
) {
404 return leaf::new_error(e_unexpected_http_method
{http::verb::post
},
405 e_http_status
{http::status::bad_request
});
407 BOOST_LEAF_AUTO(response
, execute_command(request
.body()));
408 return std::make_pair(http::status::ok
, std::move(response
));
410 // For the `error_quit` command and associated error condition we have the error handler itself fail
411 // (by throwing). This means that the server will not send any response to the client, it will just
412 // shutdown the connection.
413 // This implementation showcases two aspects:
414 // - that the implementation of error handling can fail, too
415 // - how the asynchronous operation calling this error handling function reacts to this failure.
416 [](e_error_quit
const &) -> std::pair
<http::status
, std::string
> { throw std::runtime_error("error_quit"); },
417 // For the rest of error conditions we just build a message to be sent to the remote client.
418 [&](e_parse_int64_error
const &e
, e_http_status
const *status
, e_command
const *cmd
,
419 leaf::verbose_diagnostic_info
const &diag
) {
420 return make_sr(status
, boost::str(boost::format("%1% int64 parse error: %2%") % msg_prefix(cmd
) % e
.value
) +
421 diagnostic_to_str(diag
));
423 [&](e_unexpected_arg_count
const &e
, e_http_status
const *status
, e_command
const *cmd
,
424 leaf::verbose_diagnostic_info
const &diag
) {
425 return make_sr(status
,
426 boost::str(boost::format("%1% wrong argument count: %2%") % msg_prefix(cmd
) % e
.value
) +
427 diagnostic_to_str(diag
));
429 [&](e_unexpected_http_method
const &e
, e_http_status
const *status
, e_command
const *cmd
,
430 leaf::verbose_diagnostic_info
const &diag
) {
431 return make_sr(status
, boost::str(boost::format("%1% unexpected HTTP method. Expected: %2%") %
432 msg_prefix(cmd
) % e
.value
) +
433 diagnostic_to_str(diag
));
435 [&](std::exception
const & e
, e_http_status
const *status
, e_command
const *cmd
,
436 leaf::verbose_diagnostic_info
const &diag
) {
437 return make_sr(status
, boost::str(boost::format("%1% %2%") % msg_prefix(cmd
) % e
.what()) +
438 diagnostic_to_str(diag
));
440 [&](e_http_status
const *status
, e_command
const *cmd
, leaf::verbose_diagnostic_info
const &diag
) {
441 return make_sr(status
, boost::str(boost::format("%1% unknown failure") % msg_prefix(cmd
)) +
442 diagnostic_to_str(diag
));
444 response_t response
{pair_status_response
.first
, request
.version()};
445 response
.set(http::field::server
, "Example-with-" BOOST_BEAST_VERSION_STRING
);
446 response
.set(http::field::content_type
, "text/plain");
447 response
.keep_alive(request
.keep_alive());
448 pair_status_response
.second
+= "\n";
449 response
.body() = std::move(pair_status_response
.second
);
450 response
.prepare_payload();
454 int main(int argc
, char **argv
) {
455 auto msg_prefix
= [](e_last_operation
const *op
) {
457 return boost::str(boost::format("Error (%1%): ") % op
->value
);
459 return std::string("Error: ");
462 // Error handler for internal server internal errors (not communicated to the remote client).
463 auto error_handlers
= std::make_tuple(
464 [&](std::exception_ptr
const &ep
, e_last_operation
const *op
) {
465 return leaf::try_handle_all(
466 [&]() -> leaf::result
<int> { std::rethrow_exception(ep
); },
467 [&](std::exception
const & e
, leaf::verbose_diagnostic_info
const &diag
) {
468 std::cerr
<< msg_prefix(op
) << e
.what() << " (captured)" << diagnostic_to_str(diag
)
472 [&](leaf::verbose_diagnostic_info
const &diag
) {
473 std::cerr
<< msg_prefix(op
) << "unknown (captured)" << diagnostic_to_str(diag
) << std::endl
;
477 [&](std::exception
const & e
, e_last_operation
const *op
, leaf::verbose_diagnostic_info
const &diag
) {
478 std::cerr
<< msg_prefix(op
) << e
.what() << diagnostic_to_str(diag
) << std::endl
;
481 [&](error_code ec
, leaf::verbose_diagnostic_info
const &diag
, e_last_operation
const *op
) {
482 std::cerr
<< msg_prefix(op
) << ec
<< ":" << ec
.message() << diagnostic_to_str(diag
) << std::endl
;
485 [&](leaf::verbose_diagnostic_info
const &diag
, e_last_operation
const *op
) {
486 std::cerr
<< msg_prefix(op
) << "unknown" << diagnostic_to_str(diag
) << std::endl
;
490 // Top level try block and error handler.
491 // It will handle errors from starting the server for example failure to bind to a given port
492 // (e.g. ports less than 1024 if not running as root)
493 return leaf::try_handle_all(
494 [&]() -> leaf::result
<int> {
495 auto load
= leaf::on_error(e_last_operation
{"main"});
497 std::cerr
<< "Usage: " << argv
[0] << " <address> <port>" << std::endl
;
498 std::cerr
<< "Example:\n " << argv
[0] << " 0.0.0.0 8080" << std::endl
;
502 auto const address
{net::ip::make_address(argv
[1])};
503 auto const port
{static_cast<std::uint16_t>(std::atoi(argv
[2]))};
504 net::ip::tcp::endpoint
const endpoint
{address
, port
};
506 net::io_context io_context
;
508 // Start the server acceptor and wait for a client.
509 net::ip::tcp::acceptor acceptor
{io_context
, endpoint
};
511 auto local_endpoint
= acceptor
.local_endpoint();
512 auto address_try_msg
= acceptor
.local_endpoint().address().to_string();
513 if (address_try_msg
== "0.0.0.0") {
514 address_try_msg
= "localhost";
516 std::cout
<< "Server: Started on: " << local_endpoint
<< std::endl
;
517 std::cout
<< "Try in a different terminal:\n"
518 << " curl " << address_try_msg
<< ":" << local_endpoint
.port() << " -d \"\"\nor\n"
519 << " curl " << address_try_msg
<< ":" << local_endpoint
.port() << " -d \"sum 1 2 3\""
522 auto socket
= acceptor
.accept();
523 std::cout
<< "Server: Client connected: " << socket
.remote_endpoint() << std::endl
;
525 // The error context for the async operation.
526 auto error_context
= leaf::make_context(error_handlers
);
528 async_demo_rpc(socket
, error_context
, [&](leaf::result
<void> result
) {
529 // Note: In case we wanted to add some additional information to the error associated with the result
530 // we would need to activate the error-context
531 auto active_context
= activate_context(error_context
);
533 std::cout
<< "Server: Client work completed successfully" << std::endl
;
536 // Handle errors from running the server logic
537 leaf::result
<int> result_int
{result
.error()};
538 rv
= error_context
.handle_error
<int>(result_int
.error(), error_handlers
);
543 // Let the remote side know we are shutting down.
545 socket
.shutdown(net::ip::tcp::socket::shutdown_both
, ignored
);