c++httpsboost-asioboost-beastboost-coroutine

boost beast async stackless coroutine HTTPS client throws: 167772451 - application data after close notify (SSL routines)


I'm trying to develop a quick HTTPS client using boost::beast (version 1.86) and stackless coroutines. I'm throwing up an HTTPS post to api.mailgun.net.

Everything works EXCEPT RIGHT AFTER the async_shutdown is called an SSL 167772451 - application data after close notify (SSL routines) is thrown.

I'm not sure why that is. The request_/response_ pointers should not be freed at this point and all of the reading should be completed in its entirety.

Below is the code. Again, it fails right after async_shutdown is called in the last function which is the biggest co-routine. Is this error "normal" and ignorable?

/**
* Do the session
*/
struct Session
{
    boost::asio::coroutine coroutine_;
    boost::shared_ptr<http_ssl_stream> stream_;
    boost::shared_ptr<RequestType> request_;
    uint timeout_;
    std::string host_;
    std::string port_;

    std::unique_ptr<boost::asio::ip::tcp::resolver> resolver_;
    std::unique_ptr<ResponseType> response_;
    std::unique_ptr<BufferType> buffer_;

    /**
    * First call
    */
    template<typename Self>
    void operator()( Self &self )
    {
        // Set SNI Hostname (many hosts need this to handshake successfully)
        if( !SSL_set_tlsext_host_name(stream_->native_handle(), host_.c_str()) )
        {
            // Callback with error
            return self.complete( boost::beast::error_code( static_cast<int>(::ERR_get_error()), boost::asio::error::get_ssl_category() ), boost::none );
        }


        // Resolve the resolve
        resolver_.reset( new boost::asio::ip::tcp::resolver( stream_->get_executor() ) );


        // Resolve
        resolver_->async_resolve( host_, port_, std::move( self ) );
    }

    /**
    * On resolved call
    */
    template<typename Self>
    void operator()( Self &self, boost::beast::error_code error, boost::asio::ip::tcp::resolver::results_type results )
    {
        // Resolve error, quit
        if( error )
        {
            return self.complete( error, boost::none );
        }


        // Set the expiration
        boost::beast::get_lowest_layer( *stream_ ).expires_after( std::chrono::seconds( timeout_ ) );

        // Do a connnect
        boost::beast::get_lowest_layer( *stream_ ).async_connect(
                    results,
                    std::move( self )
                );
    }

    /**
    * On connected
    */
    template<typename Self>
    void operator()( Self &self, boost::beast::error_code error, boost::asio::ip::tcp::resolver::results_type::endpoint_type results )
    {
        // Connect error
        if( error )
        {
            return self.complete( error, boost::none );
        }

        // Set the expiration
        boost::beast::get_lowest_layer( *stream_ ).expires_after( std::chrono::seconds( timeout_ ) );

        // Do a handshake
        stream_->async_handshake(
                    boost::asio::ssl::stream_base::client,
                    std::move( self )
                );
    }

    /**
    * After handshake
    */
    template<typename Self>
    void operator()( Self &self, boost::beast::error_code error, std::size_t bytes_transferred=0 )
    {

        // Coroutine for easy state knowing
        BOOST_ASIO_CORO_REENTER( coroutine_ )
        {
            /*
            // Do the handshake
            BOOST_ASIO_CORO_YIELD
            {
                // Connect error
                if( error )
                    return self.complete( error, boost::none );


                // Set the expiration
                boost::beast::get_lowest_layer( *stream_ ).expires_after( std::chrono::seconds( timeout_ ) );

                // Do a handshake
                stream_->async_handshake(
                            boost::asio::ssl::stream_base::client,
                            std::move( self )
                        );
            }
            */

            // Do the write
            BOOST_ASIO_CORO_YIELD
            {
                // Handshake error
                if( error )
                {
                    return self.complete( error, boost::none );
                }

                // Set up an HTTP GET request message
                request_->version( 11 );
                //request_->body() = body_;

                // Set the expiration
                boost::beast::get_lowest_layer( *stream_ ).expires_after( std::chrono::seconds( timeout_ ) );

                // Write the request
                boost::beast::http::async_write( *stream_, *request_, std::move( self ) );
            }

            // Execute a read
            BOOST_ASIO_CORO_YIELD
            {
                // Write error
                if( error )
                {
                    return self.complete( error, boost::none );
                }

                // Create the response
                response_.reset( new ResponseType );

                // Create the buffa
                buffer_.reset( new BufferType );

                // Set the expiration
                boost::beast::get_lowest_layer( *stream_ ).expires_after( std::chrono::seconds( timeout_ ) );

                // Receive the HTTP response
                boost::beast::http::async_read( *stream_, *buffer_, *response_, std::move( self ) );
            }

            // Shutdown the socket
            BOOST_ASIO_CORO_YIELD
            {
                // Read error
                if( error )
                {
                    return self.complete( error, boost::none );
                }


                // Set the expiration
                boost::beast::get_lowest_layer( *stream_ ).expires_after( std::chrono::seconds( timeout_ ) );

                // Receive the HTTP response
                stream_->async_shutdown( std::move( self ) );
            }

            // Shutdown error
            if( error == boost::asio::error::eof or error == boost::asio::ssl::error::stream_truncated )
            {

                // Rationale:
                // http://stackoverflow.com/questions/25587403/boost-asio-ssl-async-shutdown-always-finishes-with-an-error
                error = {};
            }


            // Did we get it?
            if( error )
            {
                self.complete( error, boost::none );
                return;
            }

            // Return no error and the buffer
            self.complete( error, *response_ );
        }
    }

};


Solution

  • I reproduced it on my end, with a GET to google.com. I get the stream_truncated error as expectable in the wild. You already deal with that:

    enter image description here

    Replacing www.google.com with api.mailgun.net just removes that symptom for me:

    enter image description here

    Can you compare your implementation and check back with your own tests?

    Live On Coliru

    #include <boost/asio.hpp>
    #include <boost/asio/ssl.hpp>
    #include <boost/beast.hpp>
    #include <boost/beast/ssl.hpp>
    #include <boost/url.hpp>
    #include <iostream>
    namespace beast       = boost::beast;
    namespace http        = boost::beast::http;
    namespace net         = boost::asio;
    namespace ssl         = net::ssl;
    using url             = boost::urls::url;
    using tcp             = net::ip::tcp;
    using http_ssl_stream = boost::beast::ssl_stream<boost::beast::tcp_stream>;
    using RequestType     = http::request<http::string_body>;
    using ResponseType    = http::response<http::string_body>;
    using BufferType      = boost::beast::flat_buffer;
    
    /**
     * Do the session
     */
    struct Session {
        net::coroutine                     coroutine_; // by value
        boost::shared_ptr<http_ssl_stream> stream_;
        boost::shared_ptr<RequestType>     request_;
        uint                               timeout_;     // by value
        std::string                        host_, port_; // ephemeral
    
        std::unique_ptr<tcp::resolver> resolver_ = {}; // stable
        std::unique_ptr<ResponseType>  response_ = {}; // stable
        std::unique_ptr<BufferType>    buffer_   = {}; // stable
    
        /**
         * First call
         */
        template <typename Self> void operator()(Self& self) {
            std::cout << __LINE__ << ": " << std::endl;
            // Set SNI Hostname (many hosts need this to handshake successfully)
            if (!SSL_set_tlsext_host_name(stream_->native_handle(), host_.c_str())) {
                // Callback with error
                return self.complete(
                    beast::error_code(static_cast<int>(::ERR_get_error()), net::error::get_ssl_category()),
                    boost::none);
            }
    
            resolver_ = std::make_unique<tcp::resolver>(stream_->get_executor());
            resolver_->async_resolve(host_, port_, std::move(self));
        }
    
        /**
         * On resolved call
         */
        template <typename Self>
        void operator()(Self& self, beast::error_code error, tcp::resolver::results_type results) {
            std::cout << __LINE__ << ": " << error.message() << std::endl;
            // Resolve error, quit
            if (error) {
                return self.complete(error, boost::none);
            }
    
            // Set the expiration
            beast::get_lowest_layer(*stream_).expires_after(std::chrono::seconds(timeout_));
    
            // Do a connnect
            beast::get_lowest_layer(*stream_).async_connect(results, std::move(self));
        }
    
        /**
         * On connected
         */
        template <typename Self>
        void operator()(Self& self, beast::error_code error,
                        tcp::resolver::results_type::endpoint_type /*results*/) {
            std::cout << __LINE__ << ": " << error.message() << std::endl;
            // Connect error
            if (error) {
                return self.complete(error, boost::none);
            }
    
            // Set the expiration
            beast::get_lowest_layer(*stream_).expires_after(std::chrono::seconds(timeout_));
    
            // Do a handshake
            stream_->async_handshake(ssl::stream_base::client, std::move(self));
        }
    
        /**
         * After handshake
         */
        template <typename Self>
        void operator()(Self& self, beast::error_code error, size_t /*bytes_transferred*/ = 0) {
            // Coroutine for easy state knowing
            BOOST_ASIO_CORO_REENTER(coroutine_) {
    
                std::cout << __LINE__ << ": " << error.message() << std::endl;
                // Do the write
                BOOST_ASIO_CORO_YIELD {
                    // Handshake error
                    if (error) {
                        return self.complete(error, boost::none);
                    }
    
                    // Set up an HTTP GET request message
                    request_->version(11);
                    // request_->body() = body_;
    
                    // Set the expiration
                    beast::get_lowest_layer(*stream_).expires_after(std::chrono::seconds(timeout_));
    
                    // Write the request
                    http::async_write(*stream_, *request_, std::move(self));
                }
    
                std::cout << __LINE__ << ": " << error.message() << std::endl;
                // Execute a read
                BOOST_ASIO_CORO_YIELD {
                    // Write error
                    if (error) {
                        return self.complete(error, boost::none);
                    }
    
                    // Create the response
                    response_ = std::make_unique<ResponseType>();
    
                    // Create the buffa
                    buffer_ = std::make_unique<BufferType>();
    
                    // Set the expiration
                    beast::get_lowest_layer(*stream_).expires_after(std::chrono::seconds(timeout_));
    
                    // Receive the HTTP response
                    http::async_read(*stream_, *buffer_, *response_, std::move(self));
                }
    
                std::cout << __LINE__ << ": " << error.message() << std::endl;
                // Shutdown the socket
                BOOST_ASIO_CORO_YIELD {
                    // Read error
                    if (error) {
                        return self.complete(error, boost::none);
                    }
    
                    // Set the expiration
                    beast::get_lowest_layer(*stream_).expires_after(std::chrono::seconds(timeout_));
    
                    // Receive the HTTP response
                    stream_->async_shutdown(std::move(self));
                }
    
                std::cout << __LINE__ << ": " << error.message() << std::endl;
                // Shutdown error
                if (error == net::error::eof or error == ssl::error::stream_truncated) {
    
                    // Rationale:
                    // http://stackoverflow.com/questions/25587403/boost-asio-ssl-async-shutdown-always-finishes-with-an-error
                    error = {};
                }
    
                std::cout << __LINE__ << ": " << error.message() << std::endl;
                // Did we get it?
                if (error) {
                    self.complete(error, boost::none);
                    return;
                }
    
                // Return no error and the buffer
                self.complete(error, std::move(*response_));
            }
        }
    };
    
    template <typename Executor, typename Token> auto async_https_get(Executor ex, url what, Token&& token) {
        static ssl::context ctx(ssl::context::tlsv12_client);
    
        std::string port     = what.has_port() ? what.port() : "https";
        std::string resource = what.encoded_resource().decode();
        std::cout << "Host: " << what.host() << " Port: " << port << " Resource: " << resource << std::endl;
    
        auto request = boost::make_shared<RequestType>(http::verb::get, resource, 11, "{}");
        request->set(http::field::host, what.host());
    
        Session session{
            net::coroutine(),
            boost::make_shared<http_ssl_stream>(ex, ctx),
            request,
            5, // seconds
            what.host(),
            port,
        };
    
        return net::async_compose<Token, void(beast::error_code, boost::optional<ResponseType>)>(
            std::move(session), token, ex);
    }
    
    int main() {
        net::thread_pool ioc(1);
    
        // url what("https://www.google.com/");
        url what("https://api.mailgun.net/");
    
        async_https_get(make_strand(ioc), what,
                        [&](beast::error_code ec, boost::optional<ResponseType> response) {
                            std::cout << "--\nCompleting with error: " << ec.message() << std::endl;
                            if (!ec)
                                std::cout << "--\nResponse: " << response->base() << std::endl;
                        });
    
        ioc.join();
    }