c++boostbeast

Simple Boost Beast HTTP server is exiting prematurely


I couldn't figure out why my http server exits right after the first request. Could anyone shed some light why this is happening? It doesn't even say "Server has stopped running.". Just exits from within.

Any help will be appreciated.

main.cpp:

#include <iostream>
#include <unordered_map>
#include <memory>

#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>

#include "..\include\server2.hpp"

using namespace std;

constexpr char ADDRESS[] = "0.0.0.0";
constexpr unsigned short PORT = 8080;
constexpr char DOC_ROOT[] = "frontend";

int main(int argc, char* argv[])
{


    try
    {
        auto const address = boost::asio::ip::make_address(ADDRESS);
        unsigned short port = PORT;

        Server server(DOC_ROOT, address, port);
        server.run();
        cout << " hello" << endl;
    }
    catch (std::exception const& e)
    {
        std::cout << "Error: " << e.what() << std::endl;
        return EXIT_FAILURE;
    }

}

server2.hpp:

#pragma once

#include <iostream>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <fstream>

#include <boost/asio.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/filesystem.hpp>

#include "toggle_button.hpp"


class Server {
    const boost::asio::ip::address m_address;
    const unsigned short m_port;
    const std::string m_docroot;

    boost::asio::io_context m_ioc;
    boost::asio::ip::tcp::acceptor m_acceptor;

    std::unordered_set<std::shared_ptr<boost::asio::ip::tcp::socket>> sse_clients;

    int i;
public:
    Server(const std::string& docroot, const boost::asio::ip::address& address, const unsigned short& port) :
        m_docroot(docroot),
        m_address(address),
        m_port(port),
        m_ioc(1),
        m_acceptor(m_ioc, boost::asio::ip::tcp::endpoint(address, port)),
            i(1) {}

    void handle_accept() {
        auto new_socket = std::make_shared<boost::asio::ip::tcp::socket>(m_ioc);

        // Start asynchronous accept operation
        m_acceptor.async_accept(*new_socket, [new_socket, this](boost::system::error_code ec) {
            if (!ec) {
                // Start a new session with the accepted socket
                handle_session(std::move(new_socket));
            }
            else {
                std::cerr << "Accept error: " << ec.message() << std::endl;
            }

            // Keep accepting new connections
            std::cout << " session << " << i++ << std::endl;
            handle_accept();
            });
    }

    void run() {
        std::cout << "Server is running and accepting connections..." << std::endl;
        // Start accepting connections
        handle_accept();

        // Now run the io_context
        m_ioc.run();
        std::cout << "Server has stopped running." << std::endl;
    }

private:
    void handle_session(std::shared_ptr<boost::asio::ip::tcp::socket> socket) {
        auto buffer = std::make_shared<boost::beast::flat_buffer>(100000);
        auto request = std::make_shared<boost::beast::http::request<boost::beast::http::string_body>>();


        boost::beast::http::async_read(*socket, *buffer, *request,
            [this, socket, buffer, request](boost::system::error_code ec, std::size_t bytes_transferred) mutable {
                if (!ec) {
                    boost::beast::http::response<boost::beast::http::dynamic_body> response;
                    response.result(boost::beast::http::status::ok);
                    response.set(boost::beast::http::field::content_type, "text/html");
                    response.version(request->version());
                    response.keep_alive(false);
                    response.set(boost::beast::http::field::server, "Beast");
                    boost::beast::ostream(response.body())
                        << "<html>\n"
                        << "<head><title>Hello</title></head>\n"
                        << "<body>\n"
                        << "<h1>Hello world</h1>\n"
                        << "<p>It's just me</p>\n"
                        << "</body>\n"
                        << "</html>\n";

                    response.prepare_payload();
                    
                    boost::beast::http::async_write(
                        *socket,
                        response,
                        [socket](boost::beast::error_code ec, std::size_t)
                        {
                            (*socket).shutdown(boost::asio::ip::tcp::socket::shutdown_send, ec);
                        });
                }
                else {
                    std::cerr << "Read error: " << ec.message() << std::endl;
                }
            });
    }

};

I tried comparing this with the sample https://www.boost.org/doc/libs/1_85_0/libs/beast/example/http/server/small/http_server_small.cpp but I couldn't figure out why mine exits. They are almost similar, except for the way I structured my server class.

I am able to receive the first response, but right after server stops and the program exits.


Solution

  • It crashes:

    Already, we can suspect you mishandled the lifetime of an object. To find which, let's use ASAN/UBSAN with -fsanitize=address,undefined, e.g.

    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address,undefined")
    

    Now we get a nice error novel:

    Which, simplified says:

    ==3166915==ERROR: AddressSanitizer: heap-use-after-free on address 0x508000000050 at pc 0x00000061a052 bp 0x7ffea9d4c6a0 sp 0x7ffea9d4c698
    READ of size 2 at 0x508000000050 thread T0
        #0 0x61a051 in http::basic_fields<std::allocator<char> >::value_type::buffer() const include/boost/beast/http/impl/fields.hpp:287
        ...
        #16 0x628db3 in http::serializer<false, http::basic_dynamic_body<beast::basic_multi_buffer<std::allocator<char> > >, ... >::consume(unsigned long) include/boost/beast/http/impl/serializer.hpp:295
        #17 0x623cc8 in http::detail::write_some_op<http::detail::write_op<http::detail::write_msg_op<Server::handle_session(...)::{lambda(...)#1}::operator()(...)::{lambda(...)#1}, asio::basic_stream_socket<asio::ip::tcp, asio::any_io_executor>, false, http::basic_dynamic_body<beast::basic_multi_buffer<std::allocator<char> > >, http::basic_fields<std::allocator<char> > >, asio::basic_stream_socket<asio::ip::tcp, asio::any_io_executor>, http::detail::serializer_is_done, false, http::basic_dynamic_body<beast::basic_multi_buffer<std::allocator<char> > >, http::basic_fields<std::allocator<char> > >, asio::basic_stream_socket<asio::ip::tcp, asio::any_io_executor>, false, http::basic_dynamic_body<beast::basic_multi_buffer<std::allocator<char> > >, http::basic_fields<std::allocator<char> > >::operator()(boost::system::error_code, unsigned long) include/boost/beast/http/impl/write.hpp:135
    
    0x508000000050 is located 48 bytes inside of 88-byte region [0x508000000020,0x508000000078)
    freed by thread T0 here:
        #0 0x7d137a2fe1f8 in operator delete(void*, unsigned long) (/nix/store/vf9kvaqln7nvqf41pphd5arvx95irgwv-gcc-14.2.0-lib/lib/libasan.so.8+0xfe1f8)
        ...
        #9 0x474095 in Server::handle_session(...)::{lambda(...)#1}::operator()(...) /home/sehe/Projects/stackoverflow/server2.hpp:93
        #11 0x5185f8 in http::detail::read_msg_op<...>::operator()(...) include/boost/beast/http/impl/read.hpp:114
    
    previously allocated by thread T0 here:
        #0 0x7d137a2fd2f8 in operator new(unsigned long) (/nix/store/vf9kvaqln7nvqf41pphd5arvx95irgwv-gcc-14.2.0-lib/lib/libasan.so.8+0xfd2f8)
        #6 0x473c4d in Server::handle_session(...)::{lambda(...)#1}::operator()(...) /home/sehe/Projects/stackoverflow/server2.hpp:75
        #8 0x5185f8 in http::detail::read_msg_op<asio::basic_stream_socket<asio::ip::tcp, asio::any_io_executor>, beast::basic_flat_buffer<std::allocator<char> >, true, http::basic_string_body<char, std::char_traits<char>, std::allocator<char> >, std::allocator<char>, Server::handle_session(std::shared_ptr<asio::basic_stream_socket<asio::ip::tcp, asio::any_io_executor> >, int)::{lambda(boost::system::error_code, unsigned long)#1}>::operator()(boost::system::error_code, unsigned long) include/boost/beast/http/impl/read.hpp:114
    

    In English: the write operation is using buffers that have been destroyed. They're actually destroyed in handle_session, line 93 which is when the stack object response gets destructed.

    The allocation spot is the same (but line 75).

    The "obvious" fix is to make reponse dynamically allocated as well and capture it in the completion handler:

    void handle_session(std::shared_ptr<tcp::socket> socket, int session_id) {
        namespace http = boost::beast::http;
        using boost::beast::error_code;
        std::cout << " session << " << session_id << std::endl;
        auto buffer   = std::make_shared<boost::beast::flat_buffer>(100000);
        auto request  = std::make_shared<http::request<http::string_body>>();
        auto response = std::make_shared<http::response<http::dynamic_body>>();
    
        http::async_read( //
            *socket, *buffer, *request,
            [/*this,*/ socket, buffer, request, response](error_code ec, size_t) mutable {
                if (!ec) {
                    response->result(http::status::ok);
                    response->set(http::field::content_type, "text/html");
                    response->version(request->version());
                    response->keep_alive(false);
                    response->set(http::field::server, "Beast");
                    boost::beast::ostream(response->body()) << R"(<html>
    <head><title>Hello</title></head>
    <body>
    <h1>Hello world</h1>
    <p>It's just me</p>
    </body>
    </html>
    )";
                    response->prepare_payload();
    
                    http::async_write(*socket, *response, [socket, response](error_code ec, size_t) {
                        socket->shutdown(tcp::socket::shutdown_send, ec);
                    });
                } else {
                    std::cerr << "Read error: " << ec.message() << std::endl;
                }
            });
    }
    

    enter image description here

    IMPROVE!

    However please consider grouping the session related stuff together, instead of dynamically allocating and capturing everything separately. You'll get something like this:

    Live On Coliru

    Which still works the same, but with much less bother.