...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
This example assumes you have gone through the setup.
/** * Implements a HTTP REST API using Boost.MySQL and Boost.Beast. * The API models a simplified order management system for an online store. * Using the API, users can query the store's product catalog, create and * edit orders, and check them out for payment. * * The API defines the following endpoints: * * GET /products?search={s} Returns a list of products * GET /orders Returns all orders * GET /orders?id={} Returns a single order * POST /orders Creates a new order * POST /orders/items Adds a new order item to an existing order * DELETE /orders/items?id={} Deletes an order item * POST /orders/checkout?id={} Checks out an order * POST /orders/complete?id={} Completes an order * * Each order can have any number of order items. An order item * represents an individual product that has been added to an order. * Orders are created empty, in a 'draft' state. Items can then be * added and removed from the order. After adding the desired items, * orders can be checked out for payment. A third-party service, like Stripe, * would be used to collect the payment. For simplicity, we've left this part * out of the example. Once checked out, an order is no longer editable. * Finally, after successful payment, order are transitioned to the * 'complete' status. * * The server uses C++20 coroutines and is multi-threaded. * It also requires linking to Boost::json and Boost::url. * The database schema is defined in db_setup.sql, in the same directory as this file. * You need to source this file before running the example. */ #include <boost/mysql/any_address.hpp> #include <boost/mysql/connection_pool.hpp> #include <boost/mysql/pool_params.hpp> #include <boost/asio/awaitable.hpp> #include <boost/asio/co_spawn.hpp> #include <boost/asio/detached.hpp> #include <boost/asio/signal_set.hpp> #include <boost/asio/thread_pool.hpp> #include <boost/system/error_code.hpp> #include <cstddef> #include <cstdlib> #include <exception> #include <iostream> #include <string> #include "server.hpp" using namespace orders; namespace mysql = boost::mysql; namespace asio = boost::asio; // The number of threads to use static constexpr std::size_t num_threads = 5; int main_impl(int argc, char* argv[]) { // Check command line arguments. if (argc != 5) { std::cerr << "Usage: " << argv[0] << " <username> <password> <mysql-hostname> <port>\n"; return EXIT_FAILURE; } // Application config const char* mysql_username = argv[1]; const char* mysql_password = argv[2]; const char* mysql_hostname = argv[3]; auto port = static_cast<unsigned short>(std::stoi(argv[4])); // An event loop, where the application will run. // We will use the main thread to run the pool, too, so we use // one thread less than configured asio::thread_pool th_pool(num_threads - 1); // Create a connection pool mysql::connection_pool pool( // Use the thread pool as execution context th_pool, // Pool configuration mysql::pool_params{ // Connect using TCP, to the given hostname and using the default port .server_address = mysql::host_and_port{mysql_hostname}, // Authenticate using the given username .username = mysql_username, // Password for the above username .password = mysql_password, // Database to use when connecting .database = "boost_mysql_orders", // We're using multi-queries .multi_queries = true, // Using thread_safe will make the pool thread-safe by internally // creating and using a strand. // This allows us to share the pool between sessions, which may run // concurrently, on different threads. .thread_safe = true, } ); // Launch the MySQL pool pool.async_run(asio::detached); // A signal_set allows us to intercept SIGINT and SIGTERM and // exit gracefully asio::signal_set signals{th_pool.get_executor(), SIGINT, SIGTERM}; // Capture SIGINT and SIGTERM to perform a clean shutdown signals.async_wait([&th_pool](boost::system::error_code, int) { // Stop the execution context. This will cause main to exit th_pool.stop(); }); // Start listening for HTTP connections. This will run until the context is stopped asio::co_spawn( // Use the thread pool to run the listener coroutine th_pool, // The coroutine to run [&pool, port] { return run_server(pool, port); }, // If an exception is thrown in the listener coroutine, propagate it [](std::exception_ptr exc) { if (exc) std::rethrow_exception(exc); } ); // Attach the current thread to the thread pool. This will block // until stop() is called th_pool.attach(); // Wait until all threads have exited th_pool.join(); std::cout << "Server exiting" << std::endl; // (If we get here, it means we got a SIGINT or SIGTERM) return EXIT_SUCCESS; } int main(int argc, char** argv) { try { main_impl(argc, argv); } catch (const std::exception& err) { std::cerr << "Error: " << err.what() << std::endl; return 1; } }
// // File: types.hpp // // Contains type definitions used in the REST API and database code. // We use Boost.Describe (BOOST_DESCRIBE_STRUCT) to add reflection // capabilities to our types. This allows using Boost.MySQL // static interface (i.e. static_results<T>) to parse query results, // and Boost.JSON automatic serialization/deserialization. #include <boost/describe/class.hpp> #include <cstdint> #include <optional> #include <string> #include <string_view> #include <vector> namespace orders { // A product object, as defined in the database and in the GET /products endpoint struct product { // The unique database ID of the object. std::int64_t id; // The product's display name std::string short_name; // The product's description std::optional<std::string> descr; // The product's price, in dollar cents std::int64_t price; }; BOOST_DESCRIBE_STRUCT(product, (), (id, short_name, descr, price)) // An order object, as defined in the database and in some REST endpoints. // This object does not include the items associated to the order. struct order { // The unique database ID of the object. std::int64_t id; // The order status. One of "draft", "pending_payment" or "complete". std::string status; }; BOOST_DESCRIBE_STRUCT(order, (), (id, status)) // Constants for the order::status member inline constexpr std::string_view status_draft = "draft"; inline constexpr std::string_view status_pending_payment = "pending_payment"; inline constexpr std::string_view status_complete = "complete"; // An order item object, as defined in the database and in some REST endpoints. // Does not include the order_id database field. struct order_item { // The unique database ID of the object. std::int64_t id; // The ID of the product that this order item represents std::int64_t product_id; // The number of units of the product that this item represents. // For instance, if product_id=2 and quantity=3, // the user wants to buy 3 units of the product with ID 2. std::int64_t quantity; }; BOOST_DESCRIBE_STRUCT(order_item, (), (id, product_id, quantity)) // An order object, with its associated order items. // Used in some REST endpoints. struct order_with_items { // The unique database ID of the object. std::int64_t id; // The order status. One of "draft", "pending_payment" or "complete". std::string status; // The items associated to this order. std::vector<order_item> items; }; BOOST_DESCRIBE_STRUCT(order_with_items, (), (id, status, items)) // REST request for POST /orders/items struct add_order_item_request { // Identifies the order to which the item should be added. std::int64_t order_id; // Identifies the product that should be added to the order. std::int64_t product_id; // The number of units of the above product that should be added to the order. std::int64_t quantity; }; BOOST_DESCRIBE_STRUCT(add_order_item_request, (), (order_id, product_id, quantity)) } // namespace orders
// // File: error.hpp // // Contains an errc enumeration and the required pieces to // use it with boost::system::error_code. // We use this indirectly in the DB repository class, // when using the error codes in boost::system::result. #include <boost/system/error_category.hpp> #include <mutex> #include <string_view> #include <type_traits> namespace orders { // Error code enum for errors originated within our application enum class errc { not_found, // couldn't retrieve or modify a certain resource because it doesn't exist order_invalid_status, // an operation found an order in a status != the one expected (e.g. not editable) product_not_found, // a product referenced by a request doesn't exist }; // To use errc with boost::system::error_code, we need // to define an error category (see the cpp file). const boost::system::error_category& get_orders_category(); // Called when constructing an error_code from an errc value. inline boost::system::error_code make_error_code(errc v) { // Roughly, an error_code is an int and a category defining what the int means. return boost::system::error_code(static_cast<int>(v), get_orders_category()); } // In multi-threaded programs, using std::cerr without any locking // can result in interleaved output. // Locks a mutex guarding std::cerr to prevent this. // All uses of std::cerr should respect this. std::unique_lock<std::mutex> lock_cerr(); // A helper function for the common case where we want to log an error code void log_error(std::string_view header, boost::system::error_code ec); } // namespace orders // This specialization is required to construct error_code's from errc values template <> struct boost::system::is_error_code_enum<orders::errc> : std::true_type { };
#include <boost/system/error_category.hpp> #include <iostream> #include <mutex> #include "error.hpp" namespace { // Converts an orders::errc to string const char* error_to_string(orders::errc value) { switch (value) { case orders::errc::not_found: return "not_found"; case orders::errc::order_invalid_status: return "order_invalid_status"; case orders::errc::product_not_found: return "product_not_found"; default: return "<unknown orders::errc>"; } } // The category to be returned by get_orders_category class orders_category final : public boost::system::error_category { public: // Identifies the error category. Used when converting error_codes to string const char* name() const noexcept final override { return "orders"; } // Given a numeric error belonging to this category, convert it to a string std::string message(int ev) const final override { return error_to_string(static_cast<orders::errc>(ev)); } }; // The error category static const orders_category g_category; // The std::mutex that guards std::cerr static std::mutex g_cerr_mutex; } // namespace // // External interface // const boost::system::error_category& orders::get_orders_category() { return g_category; } std::unique_lock<std::mutex> orders::lock_cerr() { return std::unique_lock{g_cerr_mutex}; } void orders::log_error(std::string_view header, boost::system::error_code ec) { // Lock the mutex auto guard = lock_cerr(); // Logging the error code prints the number and category. Add the message, too std::cerr << header << ": " << ec << " " << ec.message() << std::endl; }
// // File: repository.hpp // #include <boost/mysql/connection_pool.hpp> #include <boost/asio/awaitable.hpp> #include <boost/system/result.hpp> #include <cstdint> #include <string_view> #include <vector> #include "types.hpp" namespace orders { // Encapsulates database logic. // If the database is unavailable, these functions throw. // Additionally, functions that may fail depending on the supplied input // return boost::system::result<T>, avoiding exceptions in common cases. class db_repository { boost::mysql::connection_pool& pool_; public: // Constructor (this is a cheap-to-construct object) db_repository(boost::mysql::connection_pool& pool) noexcept : pool_(pool) {} // Retrieves products using a full-text search boost::asio::awaitable<std::vector<product>> get_products(std::string_view search); // Retrieves all the orders in the database boost::asio::awaitable<std::vector<order>> get_orders(); // Retrieves an order by ID. // Returns an error if the ID doesn't match any order. boost::asio::awaitable<boost::system::result<order_with_items>> get_order_by_id(std::int64_t id); // Creates an empty order. Returns the created order. boost::asio::awaitable<order_with_items> create_order(); // Adds an item to an order. Retrieves the updated order. // Returns an error if the ID doesn't match any order, the order // is not editable, or the product_id doesn't match any product boost::asio::awaitable<boost::system::result<order_with_items>> add_order_item( std::int64_t order_id, std::int64_t product_id, std::int64_t quantity ); // Removes an item from an order. Retrieves the updated order. // Returns an error if the ID doesn't match any order item // or the order is not editable. boost::asio::awaitable<boost::system::result<order_with_items>> remove_order_item(std::int64_t item_id); // Checks an order out, transitioning it to the pending_payment status. // Returns an error if the ID doesn't match any order // or the order is not editable. boost::asio::awaitable<boost::system::result<order_with_items>> checkout_order(std::int64_t id); // Completes an order, transitioning it to the complete status. // Returns an error if the ID doesn't match any order // or the order is not checked out. boost::asio::awaitable<boost::system::result<order_with_items>> complete_order(std::int64_t id); }; } // namespace orders
// // File: repository.cpp // // See the db_setup.sql file in this folder for the table definitions #include <boost/mysql/connection_pool.hpp> #include <boost/mysql/static_results.hpp> #include <boost/mysql/with_params.hpp> #include <boost/asio/awaitable.hpp> #include <boost/system/result.hpp> #include <string> #include <string_view> #include <tuple> #include <vector> #include "error.hpp" #include "repository.hpp" #include "types.hpp" namespace mysql = boost::mysql; namespace asio = boost::asio; using namespace orders; asio::awaitable<std::vector<product>> db_repository::get_products(std::string_view search) { // Get a connection from the pool auto conn = co_await pool_.async_get_connection(); // Get the products using the MySQL built-in full-text search feature. // Look for the query string in the short_name and descr fields. // Parse the query results into product struct instances mysql::static_results<product> res; co_await conn->async_execute( mysql::with_params( "SELECT id, short_name, descr, price FROM products " "WHERE MATCH(short_name, descr) AGAINST({}) " "LIMIT 10", search ), res ); // By default, connections are reset after they are returned to the pool // (by using any_connection::async_reset_connection). This will reset any // session state we changed while we were using the connection // (e.g. it will deallocate any statements we prepared). // We did nothing to mutate session state, so we can tell the pool to skip // this step, providing a minor performance gain. // We use pooled_connection::return_without_reset to do this. // If an exception was raised, the connection would be reset, for safety. conn.return_without_reset(); // Return the result co_return std::vector<product>{res.rows().begin(), res.rows().end()}; } asio::awaitable<std::vector<order>> db_repository::get_orders() { // Get a connection from the pool auto conn = co_await pool_.async_get_connection(); // Get all the orders. // Parse the result into order structs. mysql::static_results<order> res; co_await conn->async_execute("SELECT id, status FROM orders", res); // We didn't mutate session state, so we can skip resetting the connection conn.return_without_reset(); // Return the result co_return std::vector<order>{res.rows().begin(), res.rows().end()}; } asio::awaitable<boost::system::result<order_with_items>> db_repository::get_order_by_id(std::int64_t id) { // Get a connection from the pool auto conn = co_await pool_.async_get_connection(); // Get a single order and all its associated items. // The transaction ensures atomicity between the two SELECTs. // We issued 4 queries, so we get 4 resultsets back. // Ignore the 1st and 4th, and parse the other two into order and order_item structs mysql::static_results<std::tuple<>, order, order_item, std::tuple<>> result; co_await conn->async_execute( mysql::with_params( "START TRANSACTION READ ONLY;" "SELECT id, status FROM orders WHERE id = {0};" "SELECT id, product_id, quantity FROM order_items WHERE order_id = {0};" "COMMIT", id ), result ); // We didn't mutate session state conn.return_without_reset(); // result.rows<N> returns the rows for the N-th resultset, as a span auto orders = result.rows<1>(); auto order_items = result.rows<2>(); // Did we find the order we're looking for? if (orders.empty()) co_return orders::errc::not_found; const order& ord = orders[0]; // If we did, compose the result co_return order_with_items{ ord.id, ord.status, {order_items.begin(), order_items.end()} }; } asio::awaitable<order_with_items> db_repository::create_order() { // Get a connection from the pool auto conn = co_await pool_.async_get_connection(); // Create the new order. // Orders are created empty, with all fields defaulted. // MySQL does not have an INSERT ... RETURNING statement, so we use // a transaction with an INSERT and a SELECT to create the order // and retrieve it atomically. // This yields 4 resultsets, one per SQL statement. // Ignore all except the SELECT, and parse it into an order struct. mysql::static_results<std::tuple<>, std::tuple<>, order, std::tuple<>> result; co_await conn->async_execute( "START TRANSACTION;" "INSERT INTO orders () VALUES ();" "SELECT id, status FROM orders WHERE id = LAST_INSERT_ID();" "COMMIT", result ); // We didn't mutate session state conn.return_without_reset(); // This must always yield one row. Return it. const order& ord = result.rows<2>().front(); co_return order_with_items{ ord.id, ord.status, {} // A newly created order never has items }; } asio::awaitable<boost::system::result<order_with_items>> db_repository::add_order_item( std::int64_t order_id, std::int64_t product_id, std::int64_t quantity ) { // Get a connection from the pool auto conn = co_await pool_.async_get_connection(); // Retrieve the order and the product. // SELECT ... FOR UPDATE places a lock on the retrieved rows, // so they're not modified by other transactions while we use them. // If you're targeting MySQL 8.0+, you can also use SELECT ... FOR SHARE. // For the product, we only need to check that it does exist, // so we get its ID and parse the returned rows into a std::tuple. mysql::static_results<std::tuple<>, order, std::tuple<std::int64_t>> result1; co_await conn->async_execute( mysql::with_params( "START TRANSACTION;" "SELECT id, status FROM orders WHERE id = {} FOR UPDATE;" "SELECT id FROM products WHERE id = {} FOR UPDATE", order_id, product_id ), result1 ); // Check that the order exists if (result1.rows<1>().empty()) { // Not found. We did mutate session state by opening a transaction, // so we can't use return_without_reset co_return orders::errc::not_found; } const order& ord = result1.rows<1>().front(); // Verify that the order is editable. // Using SELECT ... FOR UPDATE prevents race conditions with this check. if (ord.status != status_draft) { co_return orders::errc::order_invalid_status; } // Check that the product exists if (result1.rows<2>().empty()) { co_return orders::errc::product_not_found; } // Insert the new item and retrieve all the items associated to this order mysql::static_results<std::tuple<>, order_item, std::tuple<>> result2; co_await conn->async_execute( mysql::with_params( "INSERT INTO order_items (order_id, product_id, quantity) VALUES ({0}, {1}, {2});" "SELECT id, product_id, quantity FROM order_items WHERE order_id = {0};" "COMMIT", order_id, product_id, quantity ), result2 ); // If everything went well, we didn't mutate session state conn.return_without_reset(); // Compose the return value co_return order_with_items{ ord.id, ord.status, {result2.rows<1>().begin(), result2.rows<1>().end()} }; } asio::awaitable<boost::system::result<order_with_items>> db_repository::remove_order_item(std::int64_t item_id ) { // Get a connection from the pool auto conn = co_await pool_.async_get_connection(); // Retrieve the order. // SELECT ... FOR UPDATE places a lock on the order and the item, // so they're not modified by other transactions while we use them. mysql::static_results<std::tuple<>, order> result1; co_await conn->async_execute( mysql::with_params( "START TRANSACTION;" "SELECT ord.id AS id, status FROM orders ord" " JOIN order_items it ON (ord.id = it.order_id)" " WHERE it.id = {} FOR UPDATE", item_id ), result1 ); // Check that the item exists if (result1.rows<1>().empty()) { // Not found. We did mutate session state by opening a transaction, // so we can't use return_without_reset co_return orders::errc::not_found; } const order& ord = result1.rows<1>().front(); // Check that the order is editable if (ord.status != orders::status_draft) { co_return orders::errc::order_invalid_status; } // Perform the deletion and retrieve the items mysql::static_results<std::tuple<>, order_item, std::tuple<>> result2; co_await conn->async_execute( mysql::with_params( "DELETE FROM order_items WHERE id = {};" "SELECT id, product_id, quantity FROM order_items WHERE order_id = {};" "COMMIT", item_id, ord.id ), result2 ); // If everything went well, we didn't mutate session state conn.return_without_reset(); // Compose the return value co_return order_with_items{ ord.id, ord.status, {result2.rows<1>().begin(), result2.rows<1>().end()} }; } // Helper function to implement checkout_order and complete_order static asio::awaitable<boost::system::result<order_with_items>> change_order_status( mysql::connection_pool& pool, std::int64_t order_id, std::string_view original_status, // The status that the order should have std::string_view target_status // The status to transition the order to ) { // Get a connection from the pool auto conn = co_await pool.async_get_connection(); // Retrieve the order and lock it. // FOR UPDATE places an exclusive lock on the order, // preventing other concurrent transactions (including the ones // related to adding/removing items) from changing the order mysql::static_results<std::tuple<>, std::tuple<std::string>> result1; co_await conn->async_execute( mysql::with_params( "START TRANSACTION;" "SELECT status FROM orders WHERE id = {} FOR UPDATE;", order_id ), result1 ); // Check that the order exists if (result1.rows<1>().empty()) { co_return orders::errc::not_found; } // Check that the order is in the expected status if (std::get<0>(result1.rows<1>().front()) != original_status) { co_return orders::errc::order_invalid_status; } // Update the order and retrieve the order details mysql::static_results<std::tuple<>, order_item, std::tuple<>> result2; co_await conn->async_execute( mysql::with_params( "UPDATE orders SET status = {1} WHERE id = {0};" "SELECT id, product_id, quantity FROM order_items WHERE order_id = {0};" "COMMIT", order_id, target_status ), result2 ); // If everything went well, we didn't mutate session state conn.return_without_reset(); // Compose the return value co_return order_with_items{ order_id, std::string(target_status), {result2.rows<1>().begin(), result2.rows<1>().end()} }; } asio::awaitable<boost::system::result<order_with_items>> db_repository::checkout_order(std::int64_t id) { return change_order_status(pool_, id, status_draft, status_pending_payment); } asio::awaitable<boost::system::result<order_with_items>> db_repository::complete_order(std::int64_t id) { return change_order_status(pool_, id, status_pending_payment, status_complete); }
// // File: handle_request.hpp // #include <boost/mysql/connection_pool.hpp> #include <boost/asio/awaitable.hpp> #include <boost/beast/http/message.hpp> #include <boost/beast/http/string_body.hpp> namespace orders { // Handles an individual HTTP request, producing a response. // The caller of this function should use response::version, // response::keep_alive and response::prepare_payload to adjust the response. boost::asio::awaitable<boost::beast::http::response<boost::beast::http::string_body>> handle_request( const boost::beast::http::request<boost::beast::http::string_body>& request, boost::mysql::connection_pool& pool ); } // namespace orders
// // File: handle_request.cpp // // This file contains all the boilerplate code to dispatch HTTP // requests to API endpoints. Functions here end up calling // db_repository fuctions. #include <boost/mysql/connection_pool.hpp> #include <boost/mysql/diagnostics.hpp> #include <boost/mysql/error_with_diagnostics.hpp> #include <boost/asio/awaitable.hpp> #include <boost/beast/http/message.hpp> #include <boost/beast/http/status.hpp> #include <boost/beast/http/string_body.hpp> #include <boost/beast/http/verb.hpp> #include <boost/json/parse.hpp> #include <boost/json/serialize.hpp> #include <boost/json/value_from.hpp> #include <boost/json/value_to.hpp> #include <boost/system/error_code.hpp> #include <boost/system/result.hpp> #include <boost/url/parse.hpp> #include <boost/url/url_view.hpp> #include <algorithm> #include <charconv> #include <cstdint> #include <exception> #include <iostream> #include <optional> #include <string> #include <string_view> #include <system_error> #include <unordered_map> #include <vector> #include "error.hpp" #include "handle_request.hpp" #include "repository.hpp" #include "types.hpp" namespace asio = boost::asio; namespace http = boost::beast::http; namespace mysql = boost::mysql; using boost::system::result; namespace { // Helper function that logs errors thrown by db_repository // when an unexpected database error happens void log_mysql_error(boost::system::error_code ec, const mysql::diagnostics& diag) { // Lock std::cerr, to avoid race conditions auto guard = orders::lock_cerr(); // Inserting the error code only prints the number and category. Add the message, too. std::cerr << "MySQL error: " << ec << " " << ec.message(); // client_message() contains client-side generated messages that don't // contain user-input. This is usually embedded in exceptions. // When working with error codes, we need to log it explicitly if (!diag.client_message().empty()) { std::cerr << ": " << diag.client_message(); } // server_message() contains server-side messages, and thus may // contain user-supplied input. Printing it is safe. if (!diag.server_message().empty()) { std::cerr << ": " << diag.server_message(); } // Done std::cerr << std::endl; } // Attempts to parse a numeric ID from a string std::optional<std::int64_t> parse_id(std::string_view from) { std::int64_t id{}; auto res = std::from_chars(from.data(), from.data() + from.size(), id); if (res.ec != std::errc{} || res.ptr != from.data() + from.size()) return std::nullopt; return id; } // Helpers to create error responses with a single line of code http::response<http::string_body> error_response(http::status code, std::string_view msg) { http::response<http::string_body> res; res.result(code); res.body() = msg; return res; } // Like error_response, but always uses a 400 status code http::response<http::string_body> bad_request(std::string_view body) { return error_response(http::status::bad_request, body); } // Like error_response, but always uses a 500 status code and // never provides extra information that might help potential attackers. http::response<http::string_body> internal_server_error() { return error_response(http::status::internal_server_error, "Internal server error"); } // Creates a response with a serialized JSON body. // T should be a type with Boost.Describe metadata containing the // body data to be serialized template <class T> http::response<http::string_body> json_response(const T& body) { http::response<http::string_body> res; // Set the content-type header res.set("Content-Type", "application/json"); // Serialize the body data into a string and use it as the response body. // We use Boost.JSON's automatic serialization feature, which uses Boost.Describe // reflection data to generate a serialization function for us. res.body() = boost::json::serialize(boost::json::value_from(body)); // Done return res; } // Attempts to parse a string as a JSON into an object of type T. // T should be a type with Boost.Describe metadata. // We use boost::system::result, which may contain a result or an error. template <class T> result<T> parse_json(std::string_view json_string) { // Attempt to parse the request into a json::value. // This will fail if the provided body isn't valid JSON. boost::system::error_code ec; auto val = boost::json::parse(json_string, ec); if (ec) return ec; // Attempt to parse the json::value into a T. This will // fail if the provided JSON doesn't match T's shape. return boost::json::try_value_to<T>(val); } // Generates an HTTP error response based on an error code // returned by db_repository. http::response<http::string_body> response_from_db_error(boost::system::error_code ec) { if (ec.category() == orders::get_orders_category()) { switch (static_cast<orders::errc>(ec.value())) { case orders::errc::not_found: return error_response(http::status::not_found, "The referenced entity does not exist"); case orders::errc::product_not_found: return error_response( http::status::unprocessable_entity, "The referenced product does not exist" ); case orders::errc::order_invalid_status: return error_response( http::status::unprocessable_entity, "The referenced order doesn't have the status required by the operation" ); default: return internal_server_error(); } } else { return internal_server_error(); } } // Contains data associated to an HTTP request. // To be passed to individual handler functions struct request_data { // The incoming request const http::request<http::string_body>& request; // The URL the request is targeting boost::urls::url_view target; // Connection pool mysql::connection_pool& pool; orders::db_repository repo() const { return orders::db_repository(pool); } }; // // Endpoint handlers. They should be functions with signature // asio::awaitable<http::response<http::string_body>>(const request_data&). // Handlers are associated to a single URL path and HTTP method // // GET /products?search={s}: returns a list of products. // The 'search' parameter is mandatory. asio::awaitable<http::response<http::string_body>> handle_get_products(const request_data& input) { // Parse the query parameter auto params_it = input.target.params().find("search"); if (params_it == input.target.params().end()) co_return bad_request("Missing mandatory query parameter: 'search'"); auto search = (*params_it).value; // Invoke the database logic std::vector<orders::product> products = co_await input.repo().get_products(search); // Return the response co_return json_response(products); } // GET /orders: returns all orders // GET /orders?id={}: returns a single order // Both endpoints share handler because they share path and method asio::awaitable<http::response<http::string_body>> handle_get_orders(const request_data& input) { // Parse the query parameter auto params_it = input.target.params().find("id"); // Which of the two endpoints are we serving? if (params_it == input.target.params().end()) { // GET /orders // Invoke the database logic std::vector<orders::order> orders = co_await input.repo().get_orders(); // Return the response co_return json_response(orders); } else { // GET /orders?id={} // Parse the query parameter auto order_id = parse_id((*params_it).value); if (!order_id.has_value()) co_return bad_request("URL parameter 'id' should be a valid integer"); // Invoke the database logic result<orders::order_with_items> order = co_await input.repo().get_order_by_id(*order_id); if (order.has_error()) co_return response_from_db_error(order.error()); // Return the response co_return json_response(*order); } } // POST /orders: creates a new order. // Orders are created empty, so this request has no body. asio::awaitable<http::response<http::string_body>> handle_create_order(const request_data& input) { // Invoke the database logic orders::order_with_items order = co_await input.repo().create_order(); // Return the response co_return json_response(order); } // POST /orders/items: adds a new order item to an existing order. // The request has a JSON body, described by the add_order_item_request struct. asio::awaitable<http::response<http::string_body>> handle_add_order_item(const request_data& input) { // Check that the request has the appropriate content type auto it = input.request.find("Content-Type"); if (it == input.request.end() || it->value() != "application/json") co_return bad_request("Invalid Content-Type: expected 'application/json'"); // Parse the request body auto req = parse_json<orders::add_order_item_request>(input.request.body()); if (req.has_error()) co_return bad_request("Invalid JSON body"); // Invoke the database logic result<orders::order_with_items> res = co_await input.repo() .add_order_item(req->order_id, req->product_id, req->quantity); if (res.has_error()) co_return response_from_db_error(res.error()); // Return the response co_return json_response(*res); } // DELETE /orders/items?id={}: deletes an order item. // The request has no body. asio::awaitable<http::response<http::string_body>> handle_remove_order_item(const request_data& input) { // Parse the query parameter auto params_it = input.target.params().find("id"); if (params_it == input.target.params().end()) co_return bad_request("Mandatory URL parameter 'id' not found"); auto id = parse_id((*params_it).value); if (!id.has_value()) co_return bad_request("URL parameter 'id' should be a valid integer"); // Invoke the database logic result<orders::order_with_items> res = co_await input.repo().remove_order_item(*id); if (res.has_error()) co_return response_from_db_error(res.error()); // Return the response co_return json_response(*res); } // POST /orders/checkout?id={}: checks out an order. // The request has no body. asio::awaitable<http::response<http::string_body>> handle_checkout_order(const request_data& input) { // Parse the query parameter auto params_it = input.target.params().find("id"); if (params_it == input.target.params().end()) co_return bad_request("Mandatory URL parameter 'id' not found"); auto id = parse_id((*params_it).value); if (!id.has_value()) co_return bad_request("URL parameter 'id' should be a valid integer"); // Invoke the database logic result<orders::order_with_items> res = co_await input.repo().checkout_order(*id); if (res.has_error()) co_return response_from_db_error(res.error()); // Return the response co_return json_response(*res); } // POST /orders/complete?id={}: marks an order as completed. // The request has no body. asio::awaitable<http::response<http::string_body>> handle_complete_order(const request_data& input) { // Parse the query parameter auto params_it = input.target.params().find("id"); if (params_it == input.target.params().end()) co_return bad_request("Mandatory URL parameter 'id' not found"); auto id = parse_id((*params_it).value); if (!id.has_value()) co_return bad_request("URL parameter 'id' should be a valid integer"); // Invoke the database logic result<orders::order_with_items> res = co_await input.repo().complete_order(*id); if (res.has_error()) co_return response_from_db_error(res.error()); // Return the response co_return json_response(*res); } // handle_request uses a table to dispatch to each endpoint. // This is the table's element type. struct http_endpoint { // The HTTP method associated to this endpoint. http::verb method; // The endpoint handler. asio::awaitable<http::response<http::string_body>> (*handler)(const request_data&); }; // Maps from a URL path to an endpoint handler. // A URL path might be present more than once, for different methods. const std::unordered_multimap<std::string_view, http_endpoint> endpoint_table{ {"/products", {http::verb::get, &handle_get_products} }, {"/orders", {http::verb::get, &handle_get_orders} }, {"/orders", {http::verb::post, &handle_create_order} }, {"/orders/items", {http::verb::post, &handle_add_order_item} }, {"/orders/items", {http::verb::delete_, &handle_remove_order_item}}, {"/orders/checkout", {http::verb::post, &handle_checkout_order} }, {"/orders/complete", {http::verb::post, &handle_complete_order} }, }; } // namespace // External interface asio::awaitable<http::response<http::string_body>> orders::handle_request( const http::request<http::string_body>& request, mysql::connection_pool& pool ) { // Parse the request target auto target = boost::urls::parse_origin_form(request.target()); if (!target.has_value()) co_return bad_request("Invalid request target"); // Try to find an endpoint auto [it1, it2] = endpoint_table.equal_range(target->path()); if (it1 == endpoint_table.end()) co_return error_response(http::status::not_found, "The requested endpoint does not exist"); // Match the verb. The table structure that we created // allows us to distinguish between an "endpoint does not exist" error // and an "unsupported method" error. auto it3 = std::find_if(it1, it2, [&request](const std::pair<std::string_view, http_endpoint>& ep) { return ep.second.method == request.method(); }); if (it3 == it2) co_return error_response(http::status::method_not_allowed, "Unsupported HTTP method"); // Invoke the handler try { // Attempt to handle the request co_return co_await it3->second.handler(request_data{request, *target, pool}); } catch (const mysql::error_with_diagnostics& err) { // A Boost.MySQL error. This will happen if you don't have connectivity // to your database, your schema is incorrect or your credentials are invalid. // Log the error, including diagnostics log_mysql_error(err.code(), err.get_diagnostics()); // Never disclose error info to a potential attacker co_return internal_server_error(); } catch (const std::exception& err) { // Another kind of error. This indicates a programming error or a severe // server condition (e.g. out of memory). Same procedure as above. { auto guard = orders::lock_cerr(); std::cerr << "Uncaught exception: " << err.what() << std::endl; } co_return internal_server_error(); } }
// // File: server.hpp // #include <boost/mysql/connection_pool.hpp> #include <boost/asio/awaitable.hpp> namespace orders { // Launches a HTTP server that will listen on 0.0.0.0:port. // If the server fails to launch (e.g. because the port is already in use), // throws an exception. The server runs until the underlying execution // context is stopped. boost::asio::awaitable<void> run_server(boost::mysql::connection_pool& pool, unsigned short port); } // namespace orders
// // File: server.cpp // // This file contains all the boilerplate code to implement a HTTP // server. Functions here end up invoking handle_request. #include <boost/mysql/connection_pool.hpp> #include <boost/mysql/error_code.hpp> #include <boost/asio/as_tuple.hpp> #include <boost/asio/awaitable.hpp> #include <boost/asio/cancel_after.hpp> #include <boost/asio/co_spawn.hpp> #include <boost/asio/ip/address.hpp> #include <boost/asio/ip/tcp.hpp> #include <boost/asio/redirect_error.hpp> #include <boost/asio/steady_timer.hpp> #include <boost/asio/strand.hpp> #include <boost/asio/this_coro.hpp> #include <boost/beast/core/flat_buffer.hpp> #include <boost/beast/http/error.hpp> #include <boost/beast/http/message.hpp> #include <boost/beast/http/parser.hpp> #include <boost/beast/http/read.hpp> #include <boost/beast/http/string_body.hpp> #include <boost/beast/http/verb.hpp> #include <boost/beast/http/write.hpp> #include <cstdlib> #include <exception> #include <iostream> #include <string_view> #include "error.hpp" #include "handle_request.hpp" #include "server.hpp" namespace asio = boost::asio; namespace http = boost::beast::http; namespace mysql = boost::mysql; namespace { // Runs a single HTTP session until the client closes the connection. // This coroutine will be spawned on a strand, to prevent data races. asio::awaitable<void> run_http_session(asio::ip::tcp::socket sock, mysql::connection_pool& pool) { using namespace std::chrono_literals; boost::system::error_code ec; // A buffer to read incoming client requests boost::beast::flat_buffer buff; // A timer, to use with asio::cancel_after to implement timeouts. // Re-using the same timer multiple times with cancel_after // is more efficient than using raw cancel_after, // since the timer doesn't need to be re-created for every operation. asio::steady_timer timer(co_await asio::this_coro::executor); // A HTTP session might involve more than one message if // keep-alive semantics are used. Loop until the connection closes. while (true) { // Construct a new parser for each message http::request_parser<http::string_body> parser; // Apply a reasonable limit to the allowed size // of the body in bytes to prevent abuse. parser.body_limit(10000); // Read a request. redirect_error prevents exceptions from being thrown // on error. We use cancel_after to set a timeout for the overall read operation. co_await http::async_read( sock, buff, parser.get(), asio::cancel_after(timer, 60s, asio::redirect_error(ec)) ); if (ec) { if (ec == http::error::end_of_stream) { // This means they closed the connection sock.shutdown(asio::ip::tcp::socket::shutdown_send, ec); } else { // An unknown error happened orders::log_error("Error reading HTTP request: ", ec); } co_return; } const auto& request = parser.get(); // Process the request to generate a response. // This invokes the business logic, which will need to access MySQL data. // Apply a timeout to the overall request handling process. auto response = co_await asio::co_spawn( // Use the same executor as this coroutine (it will be a strand) co_await asio::this_coro::executor, // The logic to invoke [&] { return orders::handle_request(request, pool); }, // Completion token. Returns an object that can be co_await'ed asio::cancel_after(timer, 30s) ); // Adjust the response, setting fields common to all responses bool keep_alive = response.keep_alive(); response.version(request.version()); response.keep_alive(keep_alive); response.prepare_payload(); // Send the response co_await http::async_write(sock, response, asio::cancel_after(timer, 60s, asio::redirect_error(ec))); if (ec) { orders::log_error("Error writing HTTP response: ", ec); co_return; } // This means we should close the connection, usually because // the response indicated the "Connection: close" semantic. if (!keep_alive) { sock.shutdown(asio::ip::tcp::socket::shutdown_send, ec); co_return; } } } } // namespace asio::awaitable<void> orders::run_server(mysql::connection_pool& pool, unsigned short port) { // An object that allows us to accept incoming TCP connections asio::ip::tcp::acceptor acc(co_await asio::this_coro::executor); // The endpoint where the server will listen. Edit this if you want to // change the address or port we bind to. asio::ip::tcp::endpoint listening_endpoint(asio::ip::make_address("0.0.0.0"), port); // Open the acceptor acc.open(listening_endpoint.protocol()); // Allow address reuse acc.set_option(asio::socket_base::reuse_address(true)); // Bind to the server address acc.bind(listening_endpoint); // Start listening for connections acc.listen(asio::socket_base::max_listen_connections); std::cout << "Server listening at " << acc.local_endpoint() << std::endl; // Start the acceptor loop while (true) { // Accept a new connection asio::ip::tcp::socket sock = co_await acc.async_accept(); // Function implementing our session logic. // Takes ownership of the socket. // Having this as a named variable workarounds a gcc bug // (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=107288) auto session_logic = [&pool, socket = std::move(sock)]() mutable { return run_http_session(std::move(socket), pool); }; // Launch a new session for this connection. Each session gets its // own coroutine, so we can get back to listening for new connections. asio::co_spawn( // Every session gets its own strand. This prevents data races. asio::make_strand(co_await asio::this_coro::executor), // The actual coroutine std::move(session_logic), // Callback to run when the coroutine finishes [](std::exception_ptr ptr) { if (ptr) { // For extra safety, log the exception but don't propagate it. // If we failed to anticipate an error condition that ends up raising an exception, // terminate only the affected session, instead of crashing the server. try { std::rethrow_exception(ptr); } catch (const std::exception& exc) { auto guard = lock_cerr(); std::cerr << "Uncaught error in a session: " << exc.what() << std::endl; } } } ); } }