diff --git a/src/connection.hpp b/src/connection.hpp index ae96c84..e12f0e4 100644 --- a/src/connection.hpp +++ b/src/connection.hpp @@ -128,7 +128,7 @@ struct Connection : MessageSender { * ``` */ void append_message(std::span data_parts, Arena arena, - bool close_after_send = false) override; + bool close_after_send) override; /** * @brief Get a WeakRef to this connection for async operations. diff --git a/src/http_handler.cpp b/src/http_handler.cpp index ca10c09..90e8147 100644 --- a/src/http_handler.cpp +++ b/src/http_handler.cpp @@ -426,7 +426,7 @@ void HttpHandler::handle_get_metrics(Connection &conn, for (auto sv : metrics_span) { *out++ = sv; } - conn.append_message(result, std::move(state.arena)); + conn.append_message(result, std::move(state.arena), state.connection_close); state.reset(); } diff --git a/tests/test_http_handler.cpp b/tests/test_http_handler.cpp index 6c161c4..7ed4c05 100644 --- a/tests/test_http_handler.cpp +++ b/tests/test_http_handler.cpp @@ -4,8 +4,107 @@ #include "server.hpp" #include +#include +#include #include #include +#include + +TEST_CASE("HTTP pipelined responses out of order") { + weaseldb::Config config; + HttpHandler handler(config); + auto server = Server::create(config, handler, {}); + int fd = server->create_local_connection(); + + auto runThread = std::thread{[&]() { server->run(); }}; + + // Send /ok and /metrics in one write call + std::string pipelined_requests = "GET /ok HTTP/1.1\r\n" + "Host: localhost\r\n" + "Connection: keep-alive\r\n" + "\r\n" + "GET /metrics HTTP/1.1\r\n" + "Host: localhost\r\n" + "Connection: keep-alive\r\n" + "\r\n"; + + int w = write(fd, pipelined_requests.c_str(), pipelined_requests.size()); + REQUIRE(w == static_cast(pipelined_requests.size())); + + // Set socket to non-blocking + int flags = fcntl(fd, F_GETFL, 0); + fcntl(fd, F_SETFL, flags | O_NONBLOCK); + + // Read all responses with non-blocking I/O and poll + char buf[8192]; + int total_read = 0; + + bool found_ok = false; + bool found_http_response = false; + + std::string ok_response_header = "Content-Length: 2"; + + while (true) { + // Use poll to wait for data availability + struct pollfd pfd = {fd, POLLIN, 0}; + int poll_result = poll(&pfd, 1, -1); // Block indefinitely + + if (poll_result > 0 && (pfd.revents & POLLIN)) { + int r = read(fd, buf + total_read, sizeof(buf) - total_read - 1); + if (r > 0) { + printf("%.*s", r, buf + total_read); + total_read += r; + + // Check if we have what we need after each read + buf[total_read] = '\0'; + std::string current_data(buf, total_read); + + found_http_response = + current_data.find("HTTP/1.1") != std::string::npos; + found_ok = current_data.find(ok_response_header) != std::string::npos; + + // If we have both HTTP response and ok_response_header, we can proceed + // with the test + if (found_http_response && found_ok) { + break; + } + } else if (r == 0) { + REQUIRE(false); + break; // EOF + } else if (errno != EAGAIN && errno != EWOULDBLOCK) { + REQUIRE(false); + } + } + } + + buf[total_read] = '\0'; + std::string response_data(buf, total_read); + + // Ensure we found both HTTP response and ok_response_header + REQUIRE(found_http_response); + REQUIRE(found_ok); + + // Find first occurrence of ok_response_header in response body + std::size_t ok_pos = response_data.find(ok_response_header); + REQUIRE(ok_pos != std::string::npos); + + // Count HTTP response status lines before the ok_response_header + std::string before_ok = response_data.substr(0, ok_pos); + int http_response_count = 0; + std::size_t pos = 0; + while ((pos = before_ok.find("HTTP/1.1", pos)) != std::string::npos) { + http_response_count++; + pos += 8; + } + + // Assert there's exactly one HTTP response line before ok_response_header + // This would fail if /metrics response comes before /ok response + CHECK(http_response_count == 1); + + close(fd); + server->shutdown(); + runThread.join(); +} TEST_CASE("HTTP pipelined POST requests race condition") { weaseldb::Config config;