Boost
C++ Libraries
...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
This version of Boost is under active development. You are currently in the develop branch. The current version is 1.91.0.
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; } } } ); } }