Why does this previously working boost beast code suddenly fail in version 1.75.0? (Possible regression?)

  boost-asio, boost-beast, c++, kubernetes, rest

I recently upgraded from Boost 1.67.0 to Boost 1.75.0 and immediately ran into issues with boost beast in code that talks to a REST API.

The code was previously working, but now it appears to be sending garbage to the server for the content and I have absolutely no clue why.

Here is the code, which posts a JSON string to the Kubernetes API to specify a custom resource. The specifics of the REST API are immaterial as the Kubernetes API server can’t even read the boost POST request:

#include <string>
#include <iostream>
#include <sstream>
#include <fstream> 
#include <boost/beast/core.hpp>
#include <boost/beast/version.hpp>
#include <boost/beast/http.hpp>
#include <boost/asio/connect.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ssl/stream.hpp> 
#include <boost/asio/ssl/error.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>


namespace bip = boost::asio::ip;
namespace bhttp = boost::beast::http;
namespace ssl = boost::asio::ssl;

void postServiceEndpoint(std::string topicName, std::string url, 
                         std::string host, std::string port, std::string namespace)
{
  boost::asio::io_context context;
  boost::asio::ip::tcp::resolver resolver(context);  
  ssl::context sslCtx({ssl::context::sslv23_client});
  boost::asio::ssl::stream<boost::beast::tcp_stream> sslStream(context, sslCtx);
  
 
  auto const results = resolver.resolve(host, port);
  SSL_set_tlsext_host_name(sslStream.native_handle(), host.c_str());
  boost::beast::get_lowest_layer(sslStream).connect(results);
  sslStream.handshake(ssl::stream_base::client);


  //Load the bearer token for authenticating with K8s... 
  std::ifstream t("/var/run/secrets/kubernetes.io/serviceaccount/token");
  std::string str((std::istreambuf_iterator<char>(t)), 
                  std::istreambuf_iterator<char>());
  std::string bearerToken = str; 

  std::string target = "/apis/sdsendpoints.net/v1/namespaces/" + namespace + "sdsendpoints"; 
  //Because the endpoint hasn't been created yet, we cant use it in the target
  //string, but if we want to retrieve the endpoint later, we have to use its name
  //in the target string... Kubernetes's REST API be weird like that. 
  bhttp::request<bhttp::string_body> request(bhttp::verb::post, target, HTTPV1DOT1);
  request.set(bhttp::field::host, host);
  request.set("Content-Type", "application/json");
  request.set("Authorization", "Bearer " + bearerToken);
   
  boost::property_tree::ptree requestTree;
  requestTree.put("apiVersion", "sdsendpoints.net/v1");
  requestTree.put("kind", "SdsEndpoint");
  requestTree.put("metadata.name", topicName);
  requestTree.put("spec.endpointURL", url);
      
  std::stringstream jsonStream;
  boost::property_tree::write_json(jsonStream, requestTree);
  request.body() = jsonStream.str();
  request.prepare_payload();
  std::cout << "REQUEST: n" << request << std::endl;
  bhttp::write(sslStream, request);
  boost::beast::flat_buffer buffer;
  bhttp::response<bhttp::string_body> response;
  bhttp::read(sslStream, buffer, response);         
  if(response.result_int() >= 400)
  {
       std::cout << "Got failure on post endpoint: " << response.result_int() << ": " << response.result() << " : " << response.body() << std::endl;
  }
  //Cleanup the SSL socket...
  boost::system::error_code ec;
  sslStream.shutdown(ec);
  if(ec == boost::asio::error::eof)
  {
    //This is fine. I am okay with the events that are unfolding currently.
    ec.assign(0, ec.category());
  }
  if(ec)
  {
    std::cout << "Got error code: " << ec << " on socket cleanup in SSL shutdown" << std::endl;
  }
  sslStream.lowest_layer().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
  if(ec)
  { 
    std::cout << "Got error code: " << ec << " on socket cleanup in TCP socket shutdown." << std::endl;
  }

}

Running this code gives the following output for the boost HTTP request:

POST apis/sdsendpoints.net/v1/namespaces/sds-test/sdsendpoints HTTP/1.1
Host: kubernetes
Authorization: Bearer <REDACTED> 

Content-Type: application/json
Content-length: 133
{"apiVersion" : "sdsendpoints.net/v1", "kind":"SdsEndpoint","metadata":{"name":"sds-tester"},"spec":{"endpointURL":"tcp://test-host:31337"}}

Which appears to be a completely valid HTTP request.

However, what I get back from the server now (under 1.75.0 that I did not get under 1.67.0) is:

Got failure on post endpoint: 400: Bad Request : {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"the object provided is unrecognized (must be of type SdsEndpoint): couldn't get version/kind; json parse error: unexpected end of JSON input (u003cemptyu003e)","reason":"BadRequest","code":400}

Which seems to indicate the actual HTTP request is getting scrambled somehow.

Additionally, while cleaning up the SSL socket I get the error code asio.ssl:2, which makes me wonder if the scrambling is due to some kind of error in setting up the connection. However, the code appears to follow the boost 1.75.0 example for synchronous HTTP SSL connections, and both this version and the 1.67.0 form that uses a TCP socket in the ssl stream instead of a boost tcp stream fail in the same way (with the 400 error).

Thinking it might be an issue in boost property tree, I rewrote that section using boost JSON and still got the same 400 error, suggesting that the problem is not in the JSON string itself but whatever boost beast is doing in this newer version with the request.

As a final sanity check I manually http POST’d using curl:

curl -x POST -h "Host: kubernetes" -H "Authorization: Bearer <REDACTED>" -H "Content-Type: application/json" --data '{"apiVersion" : "sdsendpoints.net/v1", "kind":"SdsEndpoint","metadata":{"name":"sds-tester"},"spec":{"endpointURL":"tcp://test-host:31337"}}' https://kubernetes:6443/apis/sdsendpoints.net/v1/namespaces/sds-test/sdsendpoints/ --insecure

And curl had no problem successfully posting to the kubernetes API server (and using the JSON output generated by boost property tree no less).

So right now the focus is clearly on some kind of change in boost::beast between 1.67.0 and 1.75.0. I’m left absolutely scratching my head here wondering if there is some kind of new regression that was introduced in 1.75.0…

I have tried two different compilers for this code: GCC 4.8.5 and Intel icpc 19.1.0.166 20191121. The code is being compiled and run on RHEL 7.9.

Source: Windows Questions C++

LEAVE A COMMENT