Skip to content

HTTP connection handling for streaming responses #10409

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
julianbrost opened this issue Apr 10, 2025 · 1 comment
Open

HTTP connection handling for streaming responses #10409

julianbrost opened this issue Apr 10, 2025 · 1 comment
Labels
area/api REST API

Comments

@julianbrost
Copy link
Contributor

With the goal of supporting to stream JSON responses to clients in the fly (#10407), it would be nice if that could be done still using the central request/response handling. At the moment, it's already possible to take over a whole HTTP connection and start streaming a response, like it's done for the event streams at the moment:

server.StartStreaming();
response.result(http::status::ok);
response.set(http::field::content_type, "application/json");
IoBoundWorkSlot dontLockTheIoThread (yc);
http::async_write(stream, response, yc);
stream.async_flush(yc);
asio::const_buffer newLine ("\n", 1);
for (;;) {
auto event (subscriber.GetInbox()->Shift(yc));
if (event) {
String body = JsonEncode(event);
boost::algorithm::replace_all(body, "\n", "");
asio::const_buffer payload (body.CStr(), body.GetLength());
asio::async_write(stream, payload, yc);
asio::async_write(stream, newLine, yc);
stream.async_flush(yc);
} else if (server.Disconnected()) {
return true;
}
}

However, it would be nicer if it was possible for normal request handlers to just start writing a response, similar to how it's done in Go with http.ResponseWriter. In contrast, what's currently in Icinga 2 is more similar to http.Hijacker.

However, before doing bigger changes to the connection handling, I'd propose to reevaluate how we are handling HTTP connections at the moment. In particular, we already make use of Boost Beast, a HTTP library, so do we actually need to implement the following connection handling loop ourselves or does the library maybe even provide something better out of the box?

void HttpServerConnection::ProcessMessages(boost::asio::yield_context yc)
{
namespace beast = boost::beast;
namespace http = beast::http;
namespace ch = std::chrono;
try {
/* Do not reset the buffer in the state machine.
* EnsureValidHeaders already reads from the stream into the buffer,
* EnsureValidBody continues. ProcessRequest() actually handles the request
* and needs the full buffer.
*/
beast::flat_buffer buf;
for (;;) {
m_Seen = Utility::GetTime();
http::parser<true, http::string_body> parser;
http::response<http::string_body> response;
parser.header_limit(1024 * 1024);
parser.body_limit(-1);
response.set(http::field::server, l_ServerHeader);
if (!EnsureValidHeaders(*m_Stream, buf, parser, response, m_ShuttingDown, yc)) {
break;
}
m_Seen = Utility::GetTime();
auto start (ch::steady_clock::now());
auto& request (parser.get());
{
auto method (http::string_to_verb(request["X-Http-Method-Override"]));
if (method != http::verb::unknown) {
request.method(method);
}
}
HandleExpect100(*m_Stream, request, yc);
auto authenticatedUser (m_ApiUser);
if (!authenticatedUser) {
authenticatedUser = ApiUser::GetByAuthHeader(std::string(request[http::field::authorization]));
}
Log logMsg (LogInformation, "HttpServerConnection");
logMsg << "Request " << request.method_string() << ' ' << request.target()
<< " (from " << m_PeerAddress
<< ", user: " << (authenticatedUser ? authenticatedUser->GetName() : "<unauthenticated>")
<< ", agent: " << request[http::field::user_agent]; //operator[] - Returns the value for a field, or "" if it does not exist.
ch::steady_clock::duration cpuBoundWorkTime(0);
Defer addRespCode ([&response, start, &logMsg, &cpuBoundWorkTime]() {
logMsg << ", status: " << response.result() << ")";
if (cpuBoundWorkTime >= ch::seconds(1)) {
logMsg << " waited " << ch::duration_cast<ch::milliseconds>(cpuBoundWorkTime).count() << "ms on semaphore and";
}
logMsg << " took total " << ch::duration_cast<ch::milliseconds>(ch::steady_clock::now() - start).count() << "ms.";
});
if (!HandleAccessControl(*m_Stream, request, response, yc)) {
break;
}
if (!EnsureAcceptHeader(*m_Stream, request, response, yc)) {
break;
}
if (!EnsureAuthenticatedUser(*m_Stream, request, authenticatedUser, response, yc)) {
break;
}
if (!EnsureValidBody(*m_Stream, buf, parser, authenticatedUser, response, m_ShuttingDown, yc)) {
break;
}
m_Seen = std::numeric_limits<decltype(m_Seen)>::max();
if (!ProcessRequest(*m_Stream, request, authenticatedUser, response, *this, m_HasStartedStreaming, cpuBoundWorkTime, yc)) {
break;
}
if (request.version() != 11 || request[http::field::connection] == "close") {
break;
}
}
} catch (const std::exception& ex) {
if (!m_ShuttingDown) {
Log(LogWarning, "HttpServerConnection")
<< "Exception while processing HTTP request from " << m_PeerAddress << ": " << ex.what();
}
}
Disconnect(yc);
}

Depending on the answer, options for going further could adapting our existing code, or replacing it with what the library provides.

refs #10142 (consider that when changing the implementation, maybe this can be solved in one go or it even becomes obsolete when switching to library functions)

@julianbrost julianbrost added the area/api REST API label Apr 10, 2025
@yhabteab
Copy link
Member

In particular, we already make use of Boost Beast, a HTTP library, so do we actually need to implement the following connection handling loop ourselves or does the library maybe even provide something better out of the box?

It states in:

This library is not a client or server, but it can be used to build those things. Many examples are provided, including clients and servers, which may be used as a starting point for writing your own program.

I've also browsed the documentation and the answer is, no, the library doesn't provide such helper implementations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/api REST API
Projects
None yet
Development

No branches or pull requests

2 participants