Boost C++ Libraries Home Libraries People FAQ More

PrevUpHomeNext

A C++14 REST API server that uses asio::yield_context

This example assumes you have gone through the setup.

/**
 * 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++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?id=<id>   Retrieves a single note.
 *    PUT    /notes?id=<id>   Replaces a note, changing its title and content.
 *    DELETE /notes?id=<id>   Deletes a note.
 *
 * Notes are stored in MySQL. The note_repository class encapsulates
 * 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.
 *
 * All communication happens asynchronously. We use stackful coroutines to simplify
 * development, using asio::spawn and asio::yield_context.
 * This example requires linking to Boost::context, Boost::json and Boost::url.
 */

#include <boost/mysql/any_address.hpp>
#include <boost/mysql/connection_pool.hpp>
#include <boost/mysql/pool_params.hpp>

#include <boost/asio/detached.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/signal_set.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/thread_pool.hpp>
#include <boost/system/error_code.hpp>

#include <cstdlib>
#include <iostream>
#include <memory>
#include <string>

#include "server.hpp"

namespace asio = boost::asio;
namespace mysql = boost::mysql;
using namespace notes;

int main(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.
    asio::io_context ctx;

    // Configuration for the connection pool
    mysql::pool_params params{
        // Connect using TCP, to the given hostname and using the default port
        mysql::host_and_port{mysql_hostname},

        // Authenticate using the given username
        mysql_username,

        // Password for the above username
        mysql_password,

        // Database to use when connecting
        "boost_mysql_examples",
    };

    // 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<shared_state>(mysql::connection_pool(ctx, std::move(params)));

    // Launch the MySQL pool
    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
        ctx.stop();
    });

    // Launch the server. This will run until the context is stopped
    asio::spawn(
        // Spawn the coroutine in the io_context
        ctx,

        // 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;

    // (If we get here, it means we got a SIGINT or SIGTERM)
    return EXIT_SUCCESS;
}
//
// 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 <string>
#include <vector>

namespace notes {

struct note_t
{
    // The unique database ID of the object.
    std::int64_t id;

    // The note's title.
    std::string title;

    // The note's actual content.
    std::string content;
};
BOOST_DESCRIBE_STRUCT(note_t, (), (id, title, content))

//
// REST API requests.
//

// Used for creating and replacing notes
struct note_request_body
{
    // The title that the new note should have.
    std::string title;

    // The content that the new note should have.
    std::string content;
};
BOOST_DESCRIBE_STRUCT(note_request_body, (), (title, content))

//
// REST API responses.
//

// Used by endpoints returning several notes (like GET /notes).
struct multi_notes_response
{
    // The retrieved notes.
    std::vector<note_t> notes;
};
BOOST_DESCRIBE_STRUCT(multi_notes_response, (), (notes))

// Used by endpoints returning a single note (like GET /notes/<id>)
struct single_note_response
{
    // The retrieved note.
    note_t note;
};
BOOST_DESCRIBE_STRUCT(single_note_response, (), (note))

// Used by DELETE /notes/<id>
struct delete_note_response
{
    // true if the note was found and deleted, false if the note didn't exist.
    bool deleted;
};
BOOST_DESCRIBE_STRUCT(delete_note_response, (), (deleted))

}  // namespace notes
//
// File: repository.hpp
//

#include <boost/mysql/connection_pool.hpp>
#include <boost/mysql/string_view.hpp>

#include <boost/asio/spawn.hpp>
#include <boost/optional/optional.hpp>

#include <cstdint>

#include "types.hpp"

namespace notes {

// 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 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) noexcept : pool_(pool) {}

    // Retrieves all notes present in the database
    std::vector<note_t> 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.
    boost::optional<note_t> 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(
        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.
    boost::optional<note_t> replace_note(
        std::int64_t note_id,
        boost::mysql::string_view title,
        boost::mysql::string_view content,
        boost::asio::yield_context yield
    );

    // Deletes the note identified by note_id. Returns true if
    // a matching note was deleted, false otherwise.
    bool delete_note(std::int64_t note_id, boost::asio::yield_context yield);
};

}  // namespace notes
//
// 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 <boost/mysql/static_results.hpp>
#include <boost/mysql/string_view.hpp>
#include <boost/mysql/with_diagnostics.hpp>
#include <boost/mysql/with_params.hpp>

#include <iterator>
#include <tuple>
#include <utility>

#include "repository.hpp"
#include "types.hpp"

namespace asio = boost::asio;
namespace mysql = boost::mysql;
using namespace notes;
using mysql::with_diagnostics;

std::vector<note_t> 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
    // pool when the proxy object is destroyed.
    // with_diagnostics ensures that thrown exceptions include diagnostic information
    mysql::pooled_connection conn = pool_.async_get_connection(with_diagnostics(yield));

    // Execute the query to retrieve all notes. We use the static interface to
    // parse results directly into static_results.
    mysql::static_results<note_t> result;
    conn->async_execute("SELECT id, title, content FROM notes", result, with_diagnostics(yield));

    // 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.
    conn.return_without_reset();

    // Move note_t objects into the result vector to save allocations
    return std::vector<note_t>(
        std::make_move_iterator(result.rows().begin()),
        std::make_move_iterator(result.rows().end())
    );

    // If an exception is thrown, pooled_connection's destructor will
    // return the connection automatically to the pool.
}

boost::optional<note_t> 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
    // pool when the proxy object is destroyed.
    mysql::pooled_connection conn = pool_.async_get_connection(with_diagnostics(yield));

    // When executed, with_params expands a query client-side before sending it to the server.
    // Placeholders are marked with {}
    mysql::static_results<note_t> result;
    conn->async_execute(
        mysql::with_params("SELECT id, title, content FROM notes WHERE id = {}", note_id),
        result,
        with_diagnostics(yield)
    );

    // We did nothing to mutate session state, so we can skip reset
    conn.return_without_reset();

    // An empty results object indicates that no note was found
    if (result.rows().empty())
        return {};
    else
        return std::move(result.rows()[0]);
}

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
    // pool when the proxy object is destroyed.
    mysql::pooled_connection conn = pool_.async_get_connection(with_diagnostics(yield));

    // We will use statements in this function for the sake of example.
    // We don't need to deallocate the statement explicitly,
    // since the pool takes care of it after the connection is returned.
    // You can also use with_params instead of statements.
    mysql::statement stmt = conn->async_prepare_statement(
        "INSERT INTO notes (title, content) VALUES (?, ?)",
        with_diagnostics(yield)
    );

    // Execute the statement. The statement won't produce any rows,
    // so we can use static_results<std::tuple<>>
    mysql::static_results<std::tuple<>> result;
    conn->async_execute(stmt.bind(title, content), result, with_diagnostics(yield));

    // MySQL reports last_insert_id as a uint64_t regardless of the actual ID type.
    // Given our table definition, this cast is safe
    auto new_id = static_cast<std::int64_t>(result.last_insert_id());

    return note_t{new_id, title, content};

    // There's no need to return the connection explicitly to the pool,
    // pooled_connection's destructor takes care of it.
}

boost::optional<note_t> note_repository::replace_note(
    std::int64_t note_id,
    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
    // pool when the proxy object is destroyed.
    mysql::pooled_connection conn = pool_.async_get_connection(with_diagnostics(yield));

    // Expand and execute the query.
    // It won't produce any rows, so we can use static_results<std::tuple<>>
    mysql::static_results<std::tuple<>> empty_result;
    conn->async_execute(
        mysql::with_params(
            "UPDATE notes SET title = {}, content = {} WHERE id = {}",
            title,
            content,
            note_id
        ),
        empty_result,
        with_diagnostics(yield)
    );

    // We didn't mutate session state, so we can skip reset
    conn.return_without_reset();

    // No affected rows means that the note doesn't exist
    if (empty_result.affected_rows() == 0u)
        return {};

    return note_t{note_id, title, content};
}

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
    // pool when the proxy object is destroyed.
    mysql::pooled_connection conn = pool_.async_get_connection(with_diagnostics(yield));

    // Expand and execute the query.
    // It won't produce any rows, so we can use static_results<std::tuple<>>
    mysql::static_results<std::tuple<>> empty_result;
    conn->async_execute(
        mysql::with_params("DELETE FROM notes WHERE id = {}", note_id),
        empty_result,
        with_diagnostics(yield)
    );

    // We didn't mutate session state, so we can skip reset
    conn.return_without_reset();

    // No affected rows means that the note didn't exist
    return empty_result.affected_rows() != 0u;
}
//
// File: handle_request.hpp
//

#include <boost/mysql/connection_pool.hpp>

#include <boost/asio/spawn.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.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<boost::beast::http::string_body> handle_request(
    boost::mysql::connection_pool& pool,
    const boost::beast::http::request<boost::beast::http::string_body>& request,
    boost::asio::yield_context yield
);

}  // namespace notes
//
// 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 <boost/mysql/error_code.hpp>
#include <boost/mysql/error_with_diagnostics.hpp>
#include <boost/mysql/string_view.hpp>

#include <boost/asio/spawn.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.hpp>
#include <boost/charconv/from_chars.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/optional/optional.hpp>
#include <boost/url/parse.hpp>

#include <cstdint>
#include <exception>
#include <iostream>
#include <string>

#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<std::int64_t> 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<http::string_body> error_response(http::status code, const char* 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(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<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;
}

// Returns true if the request's Content-Type is set to JSON
bool has_json_content_type(const http::request<http::string_body>& 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 <class T>
boost::system::result<T> 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<T>(val);
}

// 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;

    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=<note-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<http::string_body> 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<http::string_body> 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<note_request_body>(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=<note-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<http::string_body> 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<note_request_body>(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/<note-id>: deletes a note.
// The request doesn't have a body.
// The response has a JSON body with delete_note_response format.
http::response<http::string_body> 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<http::string_body> notes::handle_request(
    mysql::connection_pool& pool,
    const http::request<http::string_body>& 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();
    }
}
//
// File: server.hpp
//

#include <boost/mysql/connection_pool.hpp>

#include <boost/asio/spawn.hpp>

#include <memory>

namespace notes {

// State shared by all sessions created by our server.
// For this application, we only need a connection_pool object.
// Place here any other singleton objects your application may need.
// We will use std::shared_ptr<shared_state> to ensure that objects
// are kept alive until all sessions are terminated.
struct shared_state
{
    boost::mysql::connection_pool pool;

    shared_state(boost::mysql::connection_pool pool) noexcept : pool(std::move(pool)) {}
};

// 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),
// throws an exception. The server runs until the underlying execution
// context is stopped.
void run_server(std::shared_ptr<shared_state> st, unsigned short port, boost::asio::yield_context yield);

}  // namespace notes
//
// File: server.cpp
//
// This file contains all the boilerplate code to implement a HTTP
// server. Functions here end up invoking handle_request.

#include <boost/asio/cancel_after.hpp>
#include <boost/asio/ip/address.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.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/write.hpp>

#include <cstdlib>
#include <exception>
#include <iostream>
#include <memory>

#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<shared_state> 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<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. 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<shared_state> 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;
                    }
                }
            }
        );
    }
}

PrevUpHomeNext