From 07200f17c28293e910151abbc46d22eeff944384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anarthal=20=28Rub=C3=A9n=20P=C3=A9rez=29?= <34971811+anarthal@users.noreply.github.com> Date: Fri, 14 Feb 2025 21:13:01 +0100 Subject: [PATCH] Made the asio::yield_context HTTP server single-threaded and reworked it The example is now much more legible The example no longer crashes on termination Renamed it to match the C++ standard it requires close #414 --- doc/qbk/00_main.qbk | 17 +- doc/qbk/21_examples.qbk | 20 +- .../handle_request.cpp | 375 ------------------ .../log_error.hpp | 53 --- .../http_server_cpp11_coroutines/server.cpp | 206 ---------- .../handle_request.cpp | 341 ++++++++++++++++ .../handle_request.hpp | 15 +- .../main.cpp | 101 +++-- .../repository.cpp | 44 +- .../repository.hpp | 34 +- .../http_server_cpp14_coroutines/server.cpp | 196 +++++++++ .../server.hpp | 23 +- .../types.hpp | 20 +- .../http_server_cpp20/handle_request.cpp | 1 - .../3_advanced/http_server_cpp20/server.cpp | 12 +- .../3_advanced/http_server_cpp20/types.hpp | 11 +- example/CMakeLists.txt | 10 +- example/Jamfile | 10 +- example/private/run_notes.py | 15 +- tools/scripts/examples_qbk.py | 21 +- 20 files changed, 698 insertions(+), 827 deletions(-) delete mode 100644 example/3_advanced/http_server_cpp11_coroutines/handle_request.cpp delete mode 100644 example/3_advanced/http_server_cpp11_coroutines/log_error.hpp delete mode 100644 example/3_advanced/http_server_cpp11_coroutines/server.cpp create mode 100644 example/3_advanced/http_server_cpp14_coroutines/handle_request.cpp rename example/3_advanced/{http_server_cpp11_coroutines => http_server_cpp14_coroutines}/handle_request.hpp (64%) rename example/3_advanced/{http_server_cpp11_coroutines => http_server_cpp14_coroutines}/main.cpp (54%) rename example/3_advanced/{http_server_cpp11_coroutines => http_server_cpp14_coroutines}/repository.cpp (91%) rename example/3_advanced/{http_server_cpp11_coroutines => http_server_cpp14_coroutines}/repository.hpp (63%) create mode 100644 example/3_advanced/http_server_cpp14_coroutines/server.cpp rename example/3_advanced/{http_server_cpp11_coroutines => http_server_cpp14_coroutines}/server.hpp (54%) rename example/3_advanced/{http_server_cpp11_coroutines => http_server_cpp14_coroutines}/types.hpp (88%) diff --git a/doc/qbk/00_main.qbk b/doc/qbk/00_main.qbk index c22b3f13..5e93ab84 100644 --- a/doc/qbk/00_main.qbk +++ b/doc/qbk/00_main.qbk @@ -158,15 +158,14 @@ END [import ../../example/3_advanced/http_server_cpp20/handle_request.cpp] [import ../../example/3_advanced/http_server_cpp20/server.hpp] [import ../../example/3_advanced/http_server_cpp20/server.cpp] -[import ../../example/3_advanced/http_server_cpp11_coroutines/main.cpp] -[import ../../example/3_advanced/http_server_cpp11_coroutines/types.hpp] -[import ../../example/3_advanced/http_server_cpp11_coroutines/repository.hpp] -[import ../../example/3_advanced/http_server_cpp11_coroutines/repository.cpp] -[import ../../example/3_advanced/http_server_cpp11_coroutines/handle_request.hpp] -[import ../../example/3_advanced/http_server_cpp11_coroutines/handle_request.cpp] -[import ../../example/3_advanced/http_server_cpp11_coroutines/server.hpp] -[import ../../example/3_advanced/http_server_cpp11_coroutines/server.cpp] -[import ../../example/3_advanced/http_server_cpp11_coroutines/log_error.hpp] +[import ../../example/3_advanced/http_server_cpp14_coroutines/main.cpp] +[import ../../example/3_advanced/http_server_cpp14_coroutines/types.hpp] +[import ../../example/3_advanced/http_server_cpp14_coroutines/repository.hpp] +[import ../../example/3_advanced/http_server_cpp14_coroutines/repository.cpp] +[import ../../example/3_advanced/http_server_cpp14_coroutines/handle_request.hpp] +[import ../../example/3_advanced/http_server_cpp14_coroutines/handle_request.cpp] +[import ../../example/3_advanced/http_server_cpp14_coroutines/server.hpp] +[import ../../example/3_advanced/http_server_cpp14_coroutines/server.cpp] [import ../../test/integration/test/snippets/prepared_statements.cpp] [import ../../test/integration/test/snippets/sql_formatting_advanced.cpp] [import ../../test/integration/test/snippets/connection_establishment.cpp] diff --git a/doc/qbk/21_examples.qbk b/doc/qbk/21_examples.qbk index 64813cb9..2d97cd65 100644 --- a/doc/qbk/21_examples.qbk +++ b/doc/qbk/21_examples.qbk @@ -360,27 +360,25 @@ This example assumes you have gone through the [link mysql.examples.setup setup] -[section:http_server_cpp11_coroutines A REST API server that uses asio::yield_context] +[section:http_server_cpp14_coroutines A C++14 REST API server that uses asio::yield_context] This example assumes you have gone through the [link mysql.examples.setup setup]. -[example_http_server_cpp11_coroutines_main_cpp] +[example_http_server_cpp14_coroutines_main_cpp] -[example_http_server_cpp11_coroutines_types_hpp] +[example_http_server_cpp14_coroutines_types_hpp] -[example_http_server_cpp11_coroutines_repository_hpp] +[example_http_server_cpp14_coroutines_repository_hpp] -[example_http_server_cpp11_coroutines_repository_cpp] +[example_http_server_cpp14_coroutines_repository_cpp] -[example_http_server_cpp11_coroutines_handle_request_hpp] +[example_http_server_cpp14_coroutines_handle_request_hpp] -[example_http_server_cpp11_coroutines_handle_request_cpp] +[example_http_server_cpp14_coroutines_handle_request_cpp] -[example_http_server_cpp11_coroutines_server_hpp] +[example_http_server_cpp14_coroutines_server_hpp] -[example_http_server_cpp11_coroutines_server_cpp] - -[example_http_server_cpp11_coroutines_log_error_hpp] +[example_http_server_cpp14_coroutines_server_cpp] [endsect] diff --git a/example/3_advanced/http_server_cpp11_coroutines/handle_request.cpp b/example/3_advanced/http_server_cpp11_coroutines/handle_request.cpp deleted file mode 100644 index 0f80f895..00000000 --- a/example/3_advanced/http_server_cpp11_coroutines/handle_request.cpp +++ /dev/null @@ -1,375 +0,0 @@ -// -// Copyright (c) 2019-2025 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -#include - -#include "log_error.hpp" - -#ifdef BOOST_MYSQL_CXX14 - -//[example_http_server_cpp11_coroutines_handle_request_cpp -// -// File: handle_request.cpp -// - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include "handle_request.hpp" -#include "repository.hpp" -#include "types.hpp" - -// This file contains all the boilerplate code to dispatch HTTP -// requests to API endpoints. Functions here end up calling -// note_repository functions. - -namespace asio = boost::asio; -namespace http = boost::beast::http; -using boost::mysql::error_code; -using boost::mysql::string_view; -using namespace notes; - -namespace { - -// Attempts to parse a numeric ID from a string. -// If you're using C++17, you can use std::from_chars, instead -static boost::optional parse_id(const std::string& from) -{ - try - { - std::size_t consumed = 0; - int res = std::stoi(from, &consumed); - if (consumed != from.size()) - return {}; - else if (res < 0) - return {}; - return res; - } - catch (const std::exception&) - { - return {}; - } -} - -// Encapsulates the logic required to match a HTTP request -// to an API endpoint, call the relevant note_repository function, -// and return an HTTP response. -class request_handler -{ - // The HTTP request we're handling. Requests are small in size, - // so we use http::request - const http::request& request_; - - // The repository to access MySQL - note_repository repo_; - - // Creates an error response - http::response error_response(http::status code, string_view msg) const - { - http::response res; - - // Set the status code - res.result(code); - - // Set the keep alive option - res.keep_alive(request_.keep_alive()); - - // Set the body - res.body() = msg; - - // Adjust the content-length field - res.prepare_payload(); - - // Done - return res; - } - - // Used when the request's Content-Type header doesn't match what we expect - http::response invalid_content_type() const - { - return error_response(http::status::bad_request, "Invalid content-type"); - } - - // Used when the request body didn't match the format we expect - http::response invalid_body() const - { - return error_response(http::status::bad_request, "Invalid body"); - } - - // Used when the request's method didn't match the ones allowed by the endpoint - http::response method_not_allowed() const - { - return error_response(http::status::method_not_allowed, "Method not allowed"); - } - - // Used when the request target couldn't be matched to any API endpoint - http::response endpoint_not_found() const - { - return error_response(http::status::not_found, "The requested resource was not found"); - } - - // Used when the user requested a note (e.g. using GET /note/ or PUT /note/) - // but the note doesn't exist - http::response note_not_found() const - { - return error_response(http::status::not_found, "The requested note was not found"); - } - - // 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 - http::response json_response(const T& body) const - { - http::response res; - - // A JSON response is always a 200 - res.result(http::status::ok); - - // Set the content-type header - res.set("Content-Type", "application/json"); - - // Set the keep-alive option - res.keep_alive(request_.keep_alive()); - - // 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)); - - // Adjust the content-length header - res.prepare_payload(); - - // Done - return res; - } - - // Returns true if the request's Content-Type is set to JSON - bool has_json_content_type() const - { - auto it = request_.find("Content-Type"); - return it != request_.end() && it->value() == "application/json"; - } - - // Attempts to parse the request body 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 - boost::system::result parse_json_request() const - { - error_code ec; - - // Attempt to parse the request into a json::value. - // This will fail if the provided body isn't valid JSON. - auto val = boost::json::parse(request_.body(), 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(val); - } - - http::response handle_request_impl(boost::asio::yield_context yield) - { - // Parse the request target. We use Boost.Url to do this. - auto url = boost::urls::parse_origin_form(request_.target()); - if (url.has_error()) - return error_response(http::status::bad_request, "Invalid request target"); - - // We will be iterating over the target's segments to determine - // which endpoint we are being requested - auto segs = url->segments(); - auto segit = segs.begin(); - auto seg = *segit++; - - // All endpoints start with /notes - if (seg != "notes") - return endpoint_not_found(); - - if (segit == segs.end()) - { - if (request_.method() == http::verb::get) - { - // GET /notes: retrieves all the notes. - // The request doesn't have a body. - // The response has a JSON body with multi_notes_response format - auto res = repo_.get_notes(yield); - return json_response(multi_notes_response{std::move(res)}); - } - else if (request_.method() == http::verb::post) - { - // POST /notes: creates a note. - // The request has a JSON body with note_request_body format. - // The response has a JSON body with single_note_response format. - - // Parse the request body - if (!has_json_content_type()) - return invalid_content_type(); - auto args = parse_json_request(); - if (args.has_error()) - return invalid_body(); - - // Actually create the note - auto res = repo_.create_note(args->title, args->content, yield); - - // Return the newly created note as response - return json_response(single_note_response{std::move(res)}); - } - else - { - return method_not_allowed(); - } - } - else - { - // The URL has the form /notes/. Parse the note ID. - auto note_id = parse_id(*segit++); - if (!note_id.has_value()) - { - return error_response( - http::status::bad_request, - "Invalid note_id specified in request target" - ); - } - - // /notes// is not a valid endpoint - if (segit != segs.end()) - return endpoint_not_found(); - - if (request_.method() == http::verb::get) - { - // GET /notes/: retrieves a single note. - // The request doesn't have a body. - // The response has a JSON body with single_note_response format - - // Get the note - auto res = repo_.get_note(*note_id, yield); - - // If we didn't find it, return a 404 error - if (!res.has_value()) - return note_not_found(); - - // Return it as response - return json_response(single_note_response{std::move(*res)}); - } - else if (request_.method() == http::verb::put) - { - // PUT /notes/: replaces a note. - // The request has a JSON body with note_request_body format. - // The response has a JSON body with single_note_response format. - - // Parse the JSON body - if (!has_json_content_type()) - return invalid_content_type(); - auto args = parse_json_request(); - if (args.has_error()) - return invalid_body(); - - // Perform the update - auto res = repo_.replace_note(*note_id, args->title, args->content, yield); - - // Check that it took effect. Otherwise, it's because the note wasn't there - if (!res.has_value()) - return note_not_found(); - - // Return the updated note as response - return json_response(single_note_response{std::move(*res)}); - } - else if (request_.method() == http::verb::delete_) - { - // DELETE /notes/: deletes a note. - // The request doesn't have a body. - // The response has a JSON body with delete_note_response format. - - // Attempt to delete the note - bool deleted = repo_.delete_note(*note_id, yield); - - // Return whether the delete was successful in the response. - // We don't fail DELETEs for notes that don't exist. - return json_response(delete_note_response{deleted}); - } - else - { - return method_not_allowed(); - } - } - } - -public: - // Constructor - request_handler(const http::request& req, note_repository repo) - : request_(req), repo_(repo) - { - } - - // Generates a response for the request passed to the constructor - http::response handle_request(boost::asio::yield_context yield) - { - try - { - // Attempt to handle the request. We use cancel_after to set - // a timeout to the overall operation - return asio::spawn( - yield.get_executor(), - [this](asio::yield_context yield2) { return handle_request_impl(yield2); }, - asio::cancel_after(std::chrono::seconds(30), yield) - ); - } - catch (const boost::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, and return a generic 500 - log_error( - "Uncaught exception: ", - err.what(), - "\nServer diagnostics: ", - err.get_diagnostics().server_message() - ); - return error_response(http::status::internal_server_error, "Internal 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. - log_error("Uncaught exception: ", err.what()); - return error_response(http::status::internal_server_error, "Internal error"); - } - } -}; - -} // namespace - -// External interface -boost::beast::http::response notes::handle_request( - const boost::beast::http::request& request, - note_repository repo, - boost::asio::yield_context yield -) -{ - return request_handler(request, repo).handle_request(yield); -} - -//] - -#endif diff --git a/example/3_advanced/http_server_cpp11_coroutines/log_error.hpp b/example/3_advanced/http_server_cpp11_coroutines/log_error.hpp deleted file mode 100644 index 01dcadf5..00000000 --- a/example/3_advanced/http_server_cpp11_coroutines/log_error.hpp +++ /dev/null @@ -1,53 +0,0 @@ -// -// Copyright (c) 2019-2025 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_COROUTINES_LOG_ERROR_HPP -#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_COROUTINES_LOG_ERROR_HPP - -//[example_http_server_cpp11_coroutines_log_error_hpp -// -// File: log_error.hpp -// - -#include -#include - -// Helper function to safely write diagnostics to std::cerr. -// Since we're in a multi-threaded environment, directly writing to std::cerr -// can lead to interleaved output, so we should synchronize calls with a mutex. -// This function is only called in rare cases (e.g. unhandled exceptions), -// so we can afford the synchronization overhead. - -namespace notes { - -// If you're in C++17+, you can write this using fold expressions -// instead of recursion. -inline void log_error_impl() {} - -template -void log_error_impl(const Arg1& arg, const Tail&... tail) -{ - std::cerr << arg; - log_error_impl(tail...); -} - -template -void log_error(const Args&... args) -{ - static std::mutex mtx; - - // Acquire the mutex, then write the passed arguments to std::cerr. - std::unique_lock lock(mtx); - log_error_impl(args...); - std::cerr << std::endl; -} - -} // namespace notes - -//] - -#endif diff --git a/example/3_advanced/http_server_cpp11_coroutines/server.cpp b/example/3_advanced/http_server_cpp11_coroutines/server.cpp deleted file mode 100644 index 362a063f..00000000 --- a/example/3_advanced/http_server_cpp11_coroutines/server.cpp +++ /dev/null @@ -1,206 +0,0 @@ -// -// Copyright (c) 2019-2025 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -#include - -#include "log_error.hpp" - -#ifdef BOOST_MYSQL_CXX14 - -//[example_http_server_cpp11_coroutines_server_cpp -// -// File: server.cpp -// - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include "handle_request.hpp" -#include "repository.hpp" -#include "server.hpp" -#include "types.hpp" - -// This file contains all the boilerplate code to implement a HTTP -// server. Functions here end up invoking handle_request. - -namespace asio = boost::asio; -namespace http = boost::beast::http; -using boost::mysql::error_code; -using namespace notes; - -namespace { - -static void run_http_session( - boost::asio::ip::tcp::socket sock, - std::shared_ptr st, - boost::asio::yield_context yield -) -{ - error_code ec; - - // A buffer to read incoming client requests - boost::beast::flat_buffer buff; - - while (true) - { - // Construct a new parser for each message - http::request_parser parser; - - // Apply a reasonable limit to the allowed size - // of the body in bytes to prevent abuse. - parser.body_limit(10000); - - // Read a request - http::async_read(sock, buff, parser.get(), yield[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 - log_error("Error reading HTTP request: ", ec); - } - return; - } - - // Process the request to generate a response. - // This invokes the business logic, which will need to access MySQL data - auto response = handle_request(parser.get(), note_repository(st->pool), yield); - - // Determine if we should close the connection - bool keep_alive = response.keep_alive(); - - // Send the response - http::async_write(sock, response, yield[ec]); - if (ec) - return log_error("Error writing HTTP response: ", ec); - - // 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); - return; - } - } -} - -// Implements the server's accept loop. The server will -// listen for connections until stopped. -static void do_accept( - asio::any_io_executor executor, // The original executor (without strands) - std::shared_ptr acceptor, - std::shared_ptr st -) -{ - acceptor->async_accept([executor, st, acceptor](error_code ec, asio::ip::tcp::socket sock) { - // If there was an error accepting the connection, exit our loop - if (ec) - return log_error("Error while accepting connection", ec); - - // Launch a new session for this connection. Each session gets its - // own stackful coroutine, so we can get back to listening for new connections. - boost::asio::spawn( - // Every session gets its own strand. This prevents data races. - asio::make_strand(executor), - - // The actual coroutine - [st, socket = std::move(sock)](boost::asio::yield_context yield) mutable { - run_http_session(std::move(socket), std::move(st), yield); - }, - - // All errors in the session are handled via error codes or by catching - // exceptions explicitly. An unhandled exception here means an error. - // Rethrowing it will propagate the exception, making io_context::run() - // to throw and terminate the program. - [](std::exception_ptr ex) { - if (ex) - std::rethrow_exception(ex); - } - ); - - // Accept a new connection - do_accept(executor, acceptor, st); - }); -} - -} // namespace - -error_code notes::launch_server( - boost::asio::any_io_executor ex, - std::shared_ptr st, - unsigned short port -) -{ - error_code ec; - - // An object that allows us to accept incoming TCP connections. - // Since we're in a multi-threaded environment, we create a strand for the acceptor, - // so all accept handlers are run serialized - auto acceptor = std::make_shared(asio::make_strand(ex)); - - // The endpoint where the server will listen. Edit this if you want to - // change the address or port we bind to. - boost::asio::ip::tcp::endpoint listening_endpoint(boost::asio::ip::make_address("0.0.0.0"), port); - - // Open the acceptor - acceptor->open(listening_endpoint.protocol(), ec); - if (ec) - return ec; - - // Allow address reuse - acceptor->set_option(asio::socket_base::reuse_address(true), ec); - if (ec) - return ec; - - // Bind to the server address - acceptor->bind(listening_endpoint, ec); - if (ec) - return ec; - - // Start listening for connections - acceptor->listen(asio::socket_base::max_listen_connections, ec); - if (ec) - return ec; - - std::cout << "Server listening at " << acceptor->local_endpoint() << std::endl; - - // Launch the acceptor loop - do_accept(std::move(ex), std::move(acceptor), std::move(st)); - - // Done - return error_code(); -} - -//] - -#endif diff --git a/example/3_advanced/http_server_cpp14_coroutines/handle_request.cpp b/example/3_advanced/http_server_cpp14_coroutines/handle_request.cpp new file mode 100644 index 00000000..4b52c4a2 --- /dev/null +++ b/example/3_advanced/http_server_cpp14_coroutines/handle_request.cpp @@ -0,0 +1,341 @@ +// +// Copyright (c) 2019-2025 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#ifdef BOOST_MYSQL_CXX14 + +//[example_http_server_cpp14_coroutines_handle_request_cpp +// +// File: handle_request.cpp +// +// This file contains all the boilerplate code to dispatch HTTP +// requests to API endpoints. Functions here end up calling +// note_repository functions. + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "handle_request.hpp" +#include "repository.hpp" +#include "types.hpp" + +namespace asio = boost::asio; +namespace mysql = boost::mysql; +namespace http = boost::beast::http; +using namespace notes; + +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) +{ + // 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. +// If you're using C++17, you can use std::from_chars, instead +boost::optional parse_id(const std::string& from) +{ + std::int64_t id{}; + auto res = boost::charconv::from_chars(from.data(), from.data() + from.size(), id); + if (res.ec != std::errc{} || res.ptr != from.data() + from.size()) + return {}; + return id; +} + +// Helpers to create error responses with a single line of code +http::response error_response(http::status code, const char* msg) +{ + http::response res; + res.result(code); + res.body() = msg; + return res; +} + +// Like error_response, but always uses a 400 status code +http::response bad_request(const char* 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 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 +http::response json_response(const T& body) +{ + http::response 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; +} + +// Returns true if the request's Content-Type is set to JSON +bool has_json_content_type(const http::request& req) +{ + auto it = req.find("Content-Type"); + return it != req.end() && it->value() == "application/json"; +} + +// 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 +boost::system::result parse_json(boost::mysql::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(val); +} + +// Contains data associated to an HTTP request. +// To be passed to individual handler functions +struct request_data +{ + // The incoming request + const http::request& request; + + // The URL the request is targeting + boost::urls::url_view target; + + // Connection pool + mysql::connection_pool& pool; + + note_repository repo() const { return note_repository(pool); } +}; + +// +// Endpoint handlers. We have a function per method. +// All of our endpoints have /notes as the URL path. +// + +// GET /notes: retrieves all the notes. +// The request doesn't have a body. +// The response has a JSON body with multi_notes_response format +// +// GET /notes?id=: retrieves a single note. +// The request doesn't have a body. +// The response has a JSON body with single_note_response format +// +// Both endpoints share path and method, so they share handler function +http::response handle_get(const request_data& input, asio::yield_context yield) +{ + // Parse the query parameter + auto params_it = input.target.params().find("id"); + + // Did the client specify an ID? + if (params_it == input.target.params().end()) + { + auto res = input.repo().get_notes(yield); + return json_response(multi_notes_response{std::move(res)}); + } + else + { + // Parse id + auto id = parse_id((*params_it).value); + if (!id.has_value()) + return bad_request("URL parameter 'id' should be a valid integer"); + + // Get the note + auto res = input.repo().get_note(*id, yield); + + // If we didn't find it, return a 404 error + if (!res.has_value()) + return error_response(http::status::not_found, "The requested note was not found"); + + // Return it as response + return json_response(single_note_response{std::move(*res)}); + } +} + +// POST /notes: creates a note. +// The request has a JSON body with note_request_body format. +// The response has a JSON body with single_note_response format. +http::response handle_post(const request_data& input, asio::yield_context yield) +{ + // Parse the request body + if (!has_json_content_type(input.request)) + return bad_request("Invalid Content-Type: expected 'application/json'"); + auto args = parse_json(input.request.body()); + if (args.has_error()) + return bad_request("Invalid JSON"); + + // Actually create the note + auto res = input.repo().create_note(args->title, args->content, yield); + + // Return the newly created note as response + return json_response(single_note_response{std::move(res)}); +} + +// PUT /notes?id=: replaces a note. +// The request has a JSON body with note_request_body format. +// The response has a JSON body with single_note_response format. +http::response handle_put(const request_data& input, asio::yield_context yield) +{ + // Parse the query parameter + auto params_it = input.target.params().find("id"); + if (params_it == input.target.params().end()) + return bad_request("Mandatory URL parameter 'id' not found"); + auto id = parse_id((*params_it).value); + if (!id.has_value()) + return bad_request("URL parameter 'id' should be a valid integer"); + + // Parse the request body + if (!has_json_content_type(input.request)) + return bad_request("Invalid Content-Type: expected 'application/json'"); + auto args = parse_json(input.request.body()); + if (args.has_error()) + return bad_request("Invalid JSON"); + + // Perform the update + auto res = input.repo().replace_note(*id, args->title, args->content, yield); + + // Check that it took effect. Otherwise, it's because the note wasn't there + if (!res.has_value()) + return bad_request("The requested note was not found"); + + // Return the updated note as response + return json_response(single_note_response{std::move(*res)}); +} + +// DELETE /notes/: deletes a note. +// The request doesn't have a body. +// The response has a JSON body with delete_note_response format. +http::response handle_delete(const request_data& input, asio::yield_context yield) +{ + // Parse the query parameter + auto params_it = input.target.params().find("id"); + if (params_it == input.target.params().end()) + return bad_request("Mandatory URL parameter 'id' not found"); + auto id = parse_id((*params_it).value); + if (!id.has_value()) + return bad_request("URL parameter 'id' should be a valid integer"); + + // Attempt to delete the note + bool deleted = input.repo().delete_note(*id, yield); + + // Return whether the delete was successful in the response. + // We don't fail DELETEs for notes that don't exist. + return json_response(delete_note_response{deleted}); +} + +} // namespace + +// External interface +http::response notes::handle_request( + mysql::connection_pool& pool, + const http::request& request, + asio::yield_context yield +) +{ + // Parse the request target + auto target = boost::urls::parse_origin_form(request.target()); + if (!target.has_value()) + return bad_request("Invalid request target"); + + // All our endpoints have /notes as path, with different verbs and parameters. + // Verify that the path matches + if (target->path() != "/notes") + return error_response(http::status::not_found, "Endpoint not found"); + + // Compose the request_data object + request_data input{request, *target, pool}; + + // Invoke the relevant handler, depending on the method + try + { + switch (input.request.method()) + { + case http::verb::get: return handle_get(input, yield); + case http::verb::post: return handle_post(input, yield); + case http::verb::put: return handle_put(input, yield); + case http::verb::delete_: return handle_delete(input, yield); + default: return error_response(http::status::method_not_allowed, "Method not allowed for /notes"); + } + } + 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 + 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. + std::cerr << "Uncaught exception: " << err.what() << std::endl; + return internal_server_error(); + } +} + +//] + +#endif diff --git a/example/3_advanced/http_server_cpp11_coroutines/handle_request.hpp b/example/3_advanced/http_server_cpp14_coroutines/handle_request.hpp similarity index 64% rename from example/3_advanced/http_server_cpp11_coroutines/handle_request.hpp rename to example/3_advanced/http_server_cpp14_coroutines/handle_request.hpp index c6d406e6..9ec9ea24 100644 --- a/example/3_advanced/http_server_cpp11_coroutines/handle_request.hpp +++ b/example/3_advanced/http_server_cpp14_coroutines/handle_request.hpp @@ -5,27 +5,28 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_COROUTINES_HANDLE_REQUEST_HPP -#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_COROUTINES_HANDLE_REQUEST_HPP +#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP14_COROUTINES_HANDLE_REQUEST_HPP +#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP14_COROUTINES_HANDLE_REQUEST_HPP -//[example_http_server_cpp11_coroutines_handle_request_hpp +//[example_http_server_cpp14_coroutines_handle_request_hpp // // File: handle_request.hpp // -#include +#include + #include #include #include -#include "repository.hpp" - namespace notes { // 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::beast::http::response handle_request( + boost::mysql::connection_pool& pool, const boost::beast::http::request& request, - note_repository repo, boost::asio::yield_context yield ); diff --git a/example/3_advanced/http_server_cpp11_coroutines/main.cpp b/example/3_advanced/http_server_cpp14_coroutines/main.cpp similarity index 54% rename from example/3_advanced/http_server_cpp11_coroutines/main.cpp rename to example/3_advanced/http_server_cpp14_coroutines/main.cpp index 403d4e7e..810c0391 100644 --- a/example/3_advanced/http_server_cpp11_coroutines/main.cpp +++ b/example/3_advanced/http_server_cpp14_coroutines/main.cpp @@ -6,33 +6,31 @@ // #include - -#include "log_error.hpp" - #ifdef BOOST_MYSQL_CXX14 -//[example_http_server_cpp11_coroutines_main_cpp +//[example_http_server_cpp14_coroutines_main_cpp /** * Implements a HTTP REST API using Boost.MySQL and Boost.Beast. * The server is asynchronous and uses asio::yield_context as its completion - * style. It only requires C++11 to work. + * style. It only requires C++14 to work. * * It implements a minimal REST API to manage notes. * A note is a simple object containing a user-defined title and content. * The REST API offers CRUD operations on such objects: - * POST /notes Creates a new note. - * GET /notes Retrieves all notes. - * GET /notes/ Retrieves a single note. - * PUT /notes/ Replaces a note, changing its title and content. - * DELETE /notes/ Deletes a note. + * POST /notes Creates a new note. + * GET /notes Retrieves all notes. + * GET /notes?id= Retrieves a single note. + * PUT /notes?id= Replaces a note, changing its title and content. + * DELETE /notes?id= Deletes a note. * * Notes are stored in MySQL. The note_repository class encapsulates - * access to MySQL, offering friendly functions to manipulate notes. + * access to MySQL, offering friendly functions to manipulate notes. * server.cpp encapsulates all the boilerplate to launch an HTTP server, - * match URLs to API endpoints, and invoke the relevant note_repository functions. + * match URLs to API endpoints, and invoke the relevant note_repository functions. + * * All communication happens asynchronously. We use stackful coroutines to simplify - * development, using boost::asio::spawn and boost::asio::yield_context. + * development, using asio::spawn and asio::yield_context. * This example requires linking to Boost::context, Boost::json and Boost::url. */ @@ -41,11 +39,12 @@ #include #include +#include #include +#include #include #include -#include #include #include #include @@ -53,11 +52,10 @@ #include "server.hpp" +namespace asio = boost::asio; +namespace mysql = boost::mysql; using namespace notes; -// The number of threads to use -static constexpr std::size_t num_threads = 5; - int main(int argc, char* argv[]) { // Check command line arguments. @@ -74,14 +72,12 @@ int main(int argc, char* argv[]) auto port = static_cast(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 - boost::asio::thread_pool th_pool(num_threads - 1); + asio::io_context ctx; // Configuration for the connection pool - boost::mysql::pool_params pool_prms{ + mysql::pool_params params{ // Connect using TCP, to the given hostname and using the default port - boost::mysql::host_and_port{mysql_hostname}, + mysql::host_and_port{mysql_hostname}, // Authenticate using the given username mysql_username, @@ -93,47 +89,40 @@ int main(int argc, char* argv[]) "boost_mysql_examples", }; - // 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. - pool_prms.thread_safe = true; - - // Create the connection pool - auto shared_st = std::make_shared( - boost::mysql::connection_pool(th_pool, std::move(pool_prms)) - ); - - // A signal_set allows us to intercept SIGINT and SIGTERM and - // exit gracefully - boost::asio::signal_set signals{th_pool.get_executor(), SIGINT, SIGTERM}; + // Create the connection pool. + // shared_state contains all singleton objects that our application may need. + // Coroutines created by asio::spawn might survive until the io_context is destroyed + // (even after io_context::stop() has been called). This is not the case for callbacks + // and C++20 coroutines. Using a shared_ptr here ensures that the pool survives long enough. + auto st = std::make_shared(mysql::connection_pool(ctx, std::move(params))); // Launch the MySQL pool - shared_st->pool.async_run(boost::asio::detached); - - // Start listening for HTTP connections. This will run until the context is stopped - auto ec = launch_server(th_pool.get_executor(), shared_st, port); - if (ec) - { - log_error("Error launching server: ", ec); - exit(EXIT_FAILURE); - } - - // Capture SIGINT and SIGTERM to perform a clean shutdown - signals.async_wait([shared_st, &th_pool](boost::system::error_code, int) { - // Cancel the pool. This will cause async_run to complete. - shared_st->pool.cancel(); + st->pool.async_run(asio::detached); + // A signal_set allows us to intercept SIGINT and SIGTERM and exit gracefully + asio::signal_set signals{ctx.get_executor(), SIGINT, SIGTERM}; + signals.async_wait([st, &ctx](boost::system::error_code, int) { // Stop the execution context. This will cause main to exit - th_pool.stop(); + ctx.stop(); }); - // Attach the current thread to the thread pool. This will block - // until stop() is called - th_pool.attach(); + // Launch the server. This will run until the context is stopped + asio::spawn( + // Spawn the coroutine in the io_context + ctx, - // Wait until all threads have exited - th_pool.join(); + // The coroutine to run + [st, port](asio::yield_context yield) { run_server(st, port, yield); }, + + // If an exception is thrown in the coroutine, propagate it + [](std::exception_ptr exc) { + if (exc) + std::rethrow_exception(exc); + } + ); + + // Run the server until stopped + ctx.run(); std::cout << "Server exiting" << std::endl; diff --git a/example/3_advanced/http_server_cpp11_coroutines/repository.cpp b/example/3_advanced/http_server_cpp14_coroutines/repository.cpp similarity index 91% rename from example/3_advanced/http_server_cpp11_coroutines/repository.cpp rename to example/3_advanced/http_server_cpp14_coroutines/repository.cpp index 17225ed4..99fc482d 100644 --- a/example/3_advanced/http_server_cpp11_coroutines/repository.cpp +++ b/example/3_advanced/http_server_cpp14_coroutines/repository.cpp @@ -6,15 +6,21 @@ // #include - #ifdef BOOST_MYSQL_CXX14 -//[example_http_server_cpp11_coroutines_repository_cpp +//[example_http_server_cpp14_coroutines_repository_cpp // // File: repository.cpp // +// SQL code to create the notes table is located under $REPO_ROOT/example/db_setup.sql +// The table looks like this: +// +// CREATE TABLE notes( +// id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, +// title TEXT NOT NULL, +// content TEXT NOT NULL +// ); -#include #include #include #include @@ -27,20 +33,12 @@ #include "repository.hpp" #include "types.hpp" -using namespace notes; +namespace asio = boost::asio; namespace mysql = boost::mysql; +using namespace notes; using mysql::with_diagnostics; -// SQL code to create the notes table is located under $REPO_ROOT/example/db_setup.sql -// The table looks like this: -// -// CREATE TABLE notes( -// id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, -// title TEXT NOT NULL, -// content TEXT NOT NULL -// ); - -std::vector note_repository::get_notes(boost::asio::yield_context yield) +std::vector note_repository::get_notes(asio::yield_context yield) { // Get a fresh connection from the pool. This returns a pooled_connection object, // which is a proxy to an any_connection object. Connections are returned to the @@ -72,7 +70,7 @@ std::vector note_repository::get_notes(boost::asio::yield_context yield) // return the connection automatically to the pool. } -optional note_repository::get_note(std::int64_t note_id, boost::asio::yield_context yield) +boost::optional note_repository::get_note(std::int64_t note_id, asio::yield_context yield) { // Get a fresh connection from the pool. This returns a pooled_connection object, // which is a proxy to an any_connection object. Connections are returned to the @@ -98,7 +96,11 @@ optional note_repository::get_note(std::int64_t note_id, boost::asio::yi return std::move(result.rows()[0]); } -note_t note_repository::create_note(string_view title, string_view content, boost::asio::yield_context yield) +note_t note_repository::create_note( + mysql::string_view title, + mysql::string_view content, + asio::yield_context yield +) { // Get a fresh connection from the pool. This returns a pooled_connection object, // which is a proxy to an any_connection object. Connections are returned to the @@ -129,11 +131,11 @@ note_t note_repository::create_note(string_view title, string_view content, boos // pooled_connection's destructor takes care of it. } -optional note_repository::replace_note( +boost::optional note_repository::replace_note( std::int64_t note_id, - string_view title, - string_view content, - boost::asio::yield_context yield + mysql::string_view title, + mysql::string_view content, + asio::yield_context yield ) { // Get a fresh connection from the pool. This returns a pooled_connection object, @@ -165,7 +167,7 @@ optional note_repository::replace_note( return note_t{note_id, title, content}; } -bool note_repository::delete_note(std::int64_t note_id, boost::asio::yield_context yield) +bool note_repository::delete_note(std::int64_t note_id, asio::yield_context yield) { // Get a fresh connection from the pool. This returns a pooled_connection object, // which is a proxy to an any_connection object. Connections are returned to the diff --git a/example/3_advanced/http_server_cpp11_coroutines/repository.hpp b/example/3_advanced/http_server_cpp14_coroutines/repository.hpp similarity index 63% rename from example/3_advanced/http_server_cpp11_coroutines/repository.hpp rename to example/3_advanced/http_server_cpp14_coroutines/repository.hpp index 3e88af2e..ca2ef1b4 100644 --- a/example/3_advanced/http_server_cpp11_coroutines/repository.hpp +++ b/example/3_advanced/http_server_cpp14_coroutines/repository.hpp @@ -5,10 +5,10 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_COROUTINES_REPOSITORY_HPP -#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_COROUTINES_REPOSITORY_HPP +#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP14_COROUTINES_REPOSITORY_HPP +#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP14_COROUTINES_REPOSITORY_HPP -//[example_http_server_cpp11_coroutines_repository_hpp +//[example_http_server_cpp14_coroutines_repository_hpp // // File: repository.hpp // @@ -16,7 +16,6 @@ #include #include -#include #include #include @@ -26,41 +25,40 @@ namespace notes { -using boost::optional; -using boost::mysql::string_view; - -// A lightweight wrapper around a connection_pool that allows -// creating, updating, retrieving and deleting notes in MySQL. -// This class encapsulates the database logic. -// All operations are async, and use stackful coroutines (boost::asio::yield_context). +// Encapsulates database logic. +// All operations are async, and use stackful coroutines (asio::yield_context). // If the database can't be contacted, or unexpected database errors are found, -// an exception of type boost::mysql::error_with_diagnostics is thrown. +// an exception of type mysql::error_with_diagnostics is thrown. class note_repository { boost::mysql::connection_pool& pool_; public: // Constructor (this is a cheap-to-construct object) - note_repository(boost::mysql::connection_pool& pool) : pool_(pool) {} + note_repository(boost::mysql::connection_pool& pool) noexcept : pool_(pool) {} // Retrieves all notes present in the database std::vector get_notes(boost::asio::yield_context yield); // Retrieves a single note by ID. Returns an empty optional // if no note with the given ID is present in the database. - optional get_note(std::int64_t note_id, boost::asio::yield_context yield); + boost::optional get_note(std::int64_t note_id, boost::asio::yield_context yield); // Creates a new note in the database with the given components. // Returns the newly created note, including the newly allocated ID. - note_t create_note(string_view title, string_view content, boost::asio::yield_context yield); + note_t create_note( + boost::mysql::string_view title, + boost::mysql::string_view content, + boost::asio::yield_context yield + ); // Replaces the note identified by note_id, setting its components to the // ones passed. Returns the updated note. If no note with ID matching // note_id can be found, an empty optional is returned. - optional replace_note( + boost::optional replace_note( std::int64_t note_id, - string_view title, - string_view content, + boost::mysql::string_view title, + boost::mysql::string_view content, boost::asio::yield_context yield ); diff --git a/example/3_advanced/http_server_cpp14_coroutines/server.cpp b/example/3_advanced/http_server_cpp14_coroutines/server.cpp new file mode 100644 index 00000000..426a1d4f --- /dev/null +++ b/example/3_advanced/http_server_cpp14_coroutines/server.cpp @@ -0,0 +1,196 @@ +// +// Copyright (c) 2019-2025 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#ifdef BOOST_MYSQL_CXX14 + +//[example_http_server_cpp14_coroutines_server_cpp +// +// File: server.cpp +// +// This file contains all the boilerplate code to implement a HTTP +// server. Functions here end up invoking handle_request. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "handle_request.hpp" +#include "server.hpp" + +namespace asio = boost::asio; +namespace http = boost::beast::http; +using namespace notes; + +namespace { + +// Runs a single HTTP session until the client closes the connection +void run_http_session(std::shared_ptr st, asio::ip::tcp::socket sock, asio::yield_context yield) +{ + 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(yield.get_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 parser; + + // Apply a reasonable limit to the allowed size + // of the body in bytes to prevent abuse. + parser.body_limit(10000); + + // Read a request. yield[ec] prevents exceptions from being thrown + // on error. We use cancel_after to set a timeout for the overall read operation. + http::async_read(sock, buff, parser.get(), asio::cancel_after(60s, yield[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 + std::cout << "Error reading HTTP request: " << ec.message() << std::endl; + } + 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 = asio::spawn( + // Use the same executor as this coroutine + yield.get_executor(), + + // The logic to invoke + [&](asio::yield_context yield2) { return handle_request(st->pool, request, yield2); }, + + // Completion token. Passing yield blocks the current coroutine + // until handle_request completes. + asio::cancel_after(timer, 30s, yield) + ); + + // 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 + http::async_write(sock, response, asio::cancel_after(60s, yield[ec])); + if (ec) + { + std::cout << "Error writing HTTP response: " << ec.message() << std::endl; + 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); + return; + } + } +} + +} // namespace + +void notes::run_server(std::shared_ptr st, unsigned short port, asio::yield_context yield) +{ + // An object that allows us to accept incoming TCP connections + asio::ip::tcp::acceptor acc(yield.get_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 = acc.async_accept(yield); + + // Launch a new session for this connection. Each session gets its + // own coroutine, so we can get back to listening for new connections. + asio::spawn( + yield.get_executor(), + + // Function implementing our session logic. + // Takes ownership of the socket. + [st, sock = std::move(sock)](asio::yield_context yield2) mutable { + return run_http_session(std::move(st), std::move(sock), yield2); + }, + + // 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) + { + std::cerr << "Uncaught error in a session: " << exc.what() << std::endl; + } + } + } + ); + } +} + +//] + +#endif diff --git a/example/3_advanced/http_server_cpp11_coroutines/server.hpp b/example/3_advanced/http_server_cpp14_coroutines/server.hpp similarity index 54% rename from example/3_advanced/http_server_cpp11_coroutines/server.hpp rename to example/3_advanced/http_server_cpp14_coroutines/server.hpp index 084e7f46..3f312641 100644 --- a/example/3_advanced/http_server_cpp11_coroutines/server.hpp +++ b/example/3_advanced/http_server_cpp14_coroutines/server.hpp @@ -5,19 +5,17 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_COROUTINES_SERVER_HPP -#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_COROUTINES_SERVER_HPP +#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP14_COROUTINES_SERVER_HPP +#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP14_COROUTINES_SERVER_HPP -//[example_http_server_cpp11_coroutines_server_hpp +//[example_http_server_cpp14_coroutines_server_hpp // // File: server.hpp // #include -#include -#include -#include +#include #include @@ -32,19 +30,14 @@ struct shared_state { boost::mysql::connection_pool pool; - shared_state(boost::mysql::connection_pool pool) : pool(std::move(pool)) {} + shared_state(boost::mysql::connection_pool pool) noexcept : pool(std::move(pool)) {} }; -// Launches a HTTP server that will listen on 0.0.0.0:port. +// Runs 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), -// returns a non-zero error code. ex should identify the io_context or thread_pool -// where the server should run. The server is run until the underlying execution +// throws an exception. The server runs until the underlying execution // context is stopped. -boost::system::error_code launch_server( - boost::asio::any_io_executor ex, - std::shared_ptr state, - unsigned short port -); +void run_server(std::shared_ptr st, unsigned short port, boost::asio::yield_context yield); } // namespace notes diff --git a/example/3_advanced/http_server_cpp11_coroutines/types.hpp b/example/3_advanced/http_server_cpp14_coroutines/types.hpp similarity index 88% rename from example/3_advanced/http_server_cpp11_coroutines/types.hpp rename to example/3_advanced/http_server_cpp14_coroutines/types.hpp index ff1e690f..e3d20e60 100644 --- a/example/3_advanced/http_server_cpp11_coroutines/types.hpp +++ b/example/3_advanced/http_server_cpp14_coroutines/types.hpp @@ -5,27 +5,25 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_COROUTINES_TYPES_HPP -#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_COROUTINES_TYPES_HPP +#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP14_COROUTINES_TYPES_HPP +#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP14_COROUTINES_TYPES_HPP -//[example_http_server_cpp11_coroutines_types_hpp +//[example_http_server_cpp14_coroutines_types_hpp // // File: types.hpp // - -#include -#include -#include - -#include -#include - // 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) to parse query results, // and Boost.JSON automatic serialization/deserialization. +#include + +#include +#include +#include + namespace notes { struct note_t diff --git a/example/3_advanced/http_server_cpp20/handle_request.cpp b/example/3_advanced/http_server_cpp20/handle_request.cpp index 3a55e2c7..a477e55c 100644 --- a/example/3_advanced/http_server_cpp20/handle_request.cpp +++ b/example/3_advanced/http_server_cpp20/handle_request.cpp @@ -22,7 +22,6 @@ #include #include -#include #include #include #include diff --git a/example/3_advanced/http_server_cpp20/server.cpp b/example/3_advanced/http_server_cpp20/server.cpp index dd19856c..5733f86f 100644 --- a/example/3_advanced/http_server_cpp20/server.cpp +++ b/example/3_advanced/http_server_cpp20/server.cpp @@ -173,16 +173,8 @@ asio::awaitable orders::run_server(mysql::connection_pool& pool, unsigned // Start the acceptor loop while (true) { - // Accept a new connection. asio::as_tuple prevents async_accept - // from throwing exceptions on failure. - auto [ec, sock] = co_await acc.async_accept(asio::as_tuple); - - // If there was an error accepting the connection, exit our loop - if (ec) - { - log_error("Error while accepting connection", ec); - co_return; - } + // Accept a new connection + asio::ip::tcp::socket sock = co_await acc.async_accept(); // Function implementing our session logic. // Takes ownership of the socket. diff --git a/example/3_advanced/http_server_cpp20/types.hpp b/example/3_advanced/http_server_cpp20/types.hpp index 54f687bb..cbd361d8 100644 --- a/example/3_advanced/http_server_cpp20/types.hpp +++ b/example/3_advanced/http_server_cpp20/types.hpp @@ -12,6 +12,11 @@ // // 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) to parse query results, +// and Boost.JSON automatic serialization/deserialization. #include @@ -21,12 +26,6 @@ #include #include -// 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) to parse query results, -// and Boost.JSON automatic serialization/deserialization. - namespace orders { // A product object, as defined in the database and in the GET /products endpoint diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index 02a39937..3b881bab 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -99,12 +99,12 @@ endif() # Advanced add_example( - http_server_cpp11_coroutines + http_server_cpp14_coroutines SOURCES - 3_advanced/http_server_cpp11_coroutines/repository.cpp - 3_advanced/http_server_cpp11_coroutines/handle_request.cpp - 3_advanced/http_server_cpp11_coroutines/server.cpp - 3_advanced/http_server_cpp11_coroutines/main.cpp + 3_advanced/http_server_cpp14_coroutines/repository.cpp + 3_advanced/http_server_cpp14_coroutines/handle_request.cpp + 3_advanced/http_server_cpp14_coroutines/server.cpp + 3_advanced/http_server_cpp14_coroutines/main.cpp LIBS Boost::context Boost::json diff --git a/example/Jamfile b/example/Jamfile index 13ed7c8d..56d641c2 100644 --- a/example/Jamfile +++ b/example/Jamfile @@ -105,11 +105,11 @@ run_example unix_socket : 2_simple/unix_socket.cpp ; # Advanced -run_example http_server_cpp11_coroutines : - 3_advanced/http_server_cpp11_coroutines/main.cpp - 3_advanced/http_server_cpp11_coroutines/repository.cpp - 3_advanced/http_server_cpp11_coroutines/handle_request.cpp - 3_advanced/http_server_cpp11_coroutines/server.cpp +run_example http_server_cpp14_coroutines : + 3_advanced/http_server_cpp14_coroutines/main.cpp + 3_advanced/http_server_cpp14_coroutines/repository.cpp + 3_advanced/http_server_cpp14_coroutines/handle_request.cpp + 3_advanced/http_server_cpp14_coroutines/server.cpp /boost/mysql/test//boost_context_lib /boost/mysql/test//boost_json_lib /boost/url//boost_url diff --git a/example/private/run_notes.py b/example/private/run_notes.py index fedfa2dc..00c5b23c 100644 --- a/example/private/run_notes.py +++ b/example/private/run_notes.py @@ -27,14 +27,14 @@ def _random_string() -> str: def _call_endpoints(port: int): - base_url = 'http://127.0.0.1:{}'.format(port) + url = 'http://127.0.0.1:{}/notes'.format(port) # Create a note note_unique = _random_string() title = 'My note {}'.format(note_unique) content = 'This is a note about {}'.format(note_unique) res = requests.post( - '{}/notes'.format(base_url), + url, json={'title': title, 'content': content} ) _check_response(res) @@ -44,7 +44,7 @@ def _call_endpoints(port: int): assert note['note']['content'] == content # Retrieve all notes - res = requests.get('{}/notes'.format(base_url)) + res = requests.get(url) _check_response(res) all_notes = res.json() assert len([n for n in all_notes['notes'] if n['id'] == note_id]) == 1 @@ -54,7 +54,8 @@ def _call_endpoints(port: int): title = 'Edited {}'.format(note_unique) content = 'This is a note an edit on {}'.format(note_unique) res = requests.put( - '{}/notes/{}'.format(base_url, note_id), + url, + params={'id': note_id}, json={'title': title, 'content': content} ) _check_response(res) @@ -64,7 +65,7 @@ def _call_endpoints(port: int): assert note['note']['content'] == content # Retrieve the note - res = requests.get('{}/notes/{}'.format(base_url, note_id)) + res = requests.get(url, params={'id': note_id}) _check_response(res) note = res.json() assert int(note['note']['id']) == note_id @@ -72,12 +73,12 @@ def _call_endpoints(port: int): assert note['note']['content'] == content # Delete the note - res = requests.delete('{}/notes/{}'.format(base_url, note_id)) + res = requests.delete(url, params={'id': note_id}) _check_response(res) assert res.json()['deleted'] == True # The note is not there - res = requests.get('{}/notes/{}'.format(base_url, note_id)) + res = requests.get(url, params={'id': note_id}) assert res.status_code == 404 diff --git a/tools/scripts/examples_qbk.py b/tools/scripts/examples_qbk.py index 78567406..f3b6c2ba 100644 --- a/tools/scripts/examples_qbk.py +++ b/tools/scripts/examples_qbk.py @@ -142,17 +142,16 @@ ADVANCED_EXAMPLES = [ '3_advanced/http_server_cpp20/server.cpp', ], 'A REST API server that uses C++20 coroutines'), - MultiExample('http_server_cpp11_coroutines', [ - '3_advanced/http_server_cpp11_coroutines/main.cpp', - '3_advanced/http_server_cpp11_coroutines/types.hpp', - '3_advanced/http_server_cpp11_coroutines/repository.hpp', - '3_advanced/http_server_cpp11_coroutines/repository.cpp', - '3_advanced/http_server_cpp11_coroutines/handle_request.hpp', - '3_advanced/http_server_cpp11_coroutines/handle_request.cpp', - '3_advanced/http_server_cpp11_coroutines/server.hpp', - '3_advanced/http_server_cpp11_coroutines/server.cpp', - '3_advanced/http_server_cpp11_coroutines/log_error.hpp', - ], 'A REST API server that uses asio::yield_context'), + MultiExample('http_server_cpp14_coroutines', [ + '3_advanced/http_server_cpp14_coroutines/main.cpp', + '3_advanced/http_server_cpp14_coroutines/types.hpp', + '3_advanced/http_server_cpp14_coroutines/repository.hpp', + '3_advanced/http_server_cpp14_coroutines/repository.cpp', + '3_advanced/http_server_cpp14_coroutines/handle_request.hpp', + '3_advanced/http_server_cpp14_coroutines/handle_request.cpp', + '3_advanced/http_server_cpp14_coroutines/server.hpp', + '3_advanced/http_server_cpp14_coroutines/server.cpp', + ], 'A C++14 REST API server that uses asio::yield_context'), ] ALL_EXAMPLES = TUTORIALS + SIMPLE_EXAMPLES + ADVANCED_EXAMPLES