Files
weaseldb/tests/test_http_handler.cpp
Andrew Noyes fac6b8de88 Add test that shows parsing issue
It's meant to show the pipelining issue. I guess we'll solve the
newly-discovered parsing issue first.
2025-09-14 22:24:02 -04:00

181 lines
5.7 KiB
C++

#include "config.hpp"
#include "connection.hpp"
#include "http_handler.hpp"
#include "server.hpp"
#include <doctest/doctest.h>
#include <fcntl.h>
#include <poll.h>
#include <string>
#include <thread>
#include <unistd.h>
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<int>(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;
HttpHandler handler(config);
auto server = Server::create(config, handler, {});
int fd = server->create_local_connection();
auto runThread = std::thread{[&]() { server->run(); }};
// Create a POST request with JSON body that requires parsing
std::string json_body = R"({
"request_id": "test-123",
"leader_id": "leader-1",
"read_version": 1,
"preconditions": [],
"operations": [{"write": {"key": "dGVzdA==", "value": "dmFsdWU="}}]
})";
std::string first_post = "POST /v1/commit HTTP/1.1\r\n"
"Host: localhost\r\n"
"Content-Type: application/json\r\n"
"Content-Length: " +
std::to_string(json_body.size()) +
"\r\n"
"Connection: keep-alive\r\n"
"\r\n" +
json_body;
std::string second_get = "GET /v1/version HTTP/1.1\r\n"
"Host: localhost\r\n"
"Connection: close\r\n"
"\r\n";
// Send POST request followed immediately by GET request
// This creates a scenario where the GET request starts parsing
// while the POST response is being written (triggering the reset)
int w1 = write(fd, first_post.c_str(), first_post.size());
REQUIRE(w1 == static_cast<int>(first_post.size()));
int w2 = write(fd, second_get.c_str(), second_get.size());
REQUIRE(w2 == static_cast<int>(second_get.size()));
// Read responses using blocking I/O (deterministic synchronization)
char buf[4096];
int total_read = 0;
int responses_found = 0;
while (total_read < 4000) {
int r = read(fd, buf + total_read, sizeof(buf) - total_read - 1);
if (r <= 0)
break;
total_read += r;
buf[total_read] = '\0';
std::string response(buf, total_read);
std::size_t pos = 0;
while ((pos = response.find("HTTP/1.1", pos)) != std::string::npos) {
responses_found++;
pos += 8;
}
if (responses_found >= 2)
break;
}
// Should get responses to both requests
// Race condition might cause parsing errors or connection issues
CHECK(responses_found >= 1); // At minimum should handle first request
close(fd);
server->shutdown();
runThread.join();
}