mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-09-13 22:41:25 +02:00
test: socket error handling in HTTPServer using ErrorSock mock socket
Implements a child class of DynSock which is used as the mock socket for HTTPServer unit tests. The ErrorSock::Send() method raises a non-permanent error on the first HTTPRequest::WriteReply() and then succeeds after the second. In httpserver_tests.cpp use this mechanism to ensure that the server retries a send operation if such an error is encountered, and cover both optimistic (worker thread WriteReply()) and non-optimistic (I/O thread SocketHandlerConnected()) send paths.
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
#include <httpserver.h>
|
||||
#include <rpc/protocol.h>
|
||||
#include <test/util/common.h>
|
||||
#include <test/util/logging.h>
|
||||
#include <test/util/setup_common.h>
|
||||
#include <util/string.h>
|
||||
|
||||
@@ -628,4 +629,122 @@ BOOST_AUTO_TEST_CASE(http_server_socket_tests)
|
||||
server.StopListening();
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(http_socket_error_tests)
|
||||
{
|
||||
// Hard-code the server's request handler to respond to each request with
|
||||
// an incremented block count.
|
||||
int height{0};
|
||||
HTTPServer server{[&](std::shared_ptr<HTTPRequest> req) {
|
||||
req->WriteReply(HTTP_OK, strprintf("height: %d\n", height++));
|
||||
}};
|
||||
|
||||
// All replies will be the same size
|
||||
static constexpr std::size_t reply_length = std::string_view{
|
||||
"HTTP/1.1 200 OK\r\n"
|
||||
"Date: Thu, 01 Jan 2026 00:00:00 GMT\r\n" // All RFC1123 dates are 29 characters
|
||||
"Content-Length: 10\r\n"
|
||||
"Content-Type: text/html; charset=ISO-8859-1\r\n"
|
||||
"\r\n"
|
||||
"height: 0\n"
|
||||
}.size();
|
||||
|
||||
/**
|
||||
* A mocked Sock derived from DynSock whose Send() only succeeds when there is more than
|
||||
* one reply being sent (send buffer length > reply_length). Otherwise it returns
|
||||
* a recoverable error (WSAEAGAIN).
|
||||
*
|
||||
* After it sends successfully once, it continues to always succeed.
|
||||
*
|
||||
* Useful for testing "try again" logic around non-blocking socket Send() failures.
|
||||
*/
|
||||
class ErrorSock : public DynSock
|
||||
{
|
||||
public:
|
||||
explicit ErrorSock(std::shared_ptr<Pipes> pipes) : DynSock{std::move(pipes)} {}
|
||||
DynSock& operator=(Sock&&) override { assert(false); return *this; }
|
||||
|
||||
ssize_t Send(const void* buf, size_t len, int flags) const override
|
||||
{
|
||||
if (len <= reply_length && !m_have_sent) {
|
||||
#ifdef WIN32
|
||||
WSASetLastError(WSAEWOULDBLOCK);
|
||||
#else
|
||||
errno = WSAEAGAIN;
|
||||
#endif
|
||||
return -1;
|
||||
} else {
|
||||
m_have_sent = true;
|
||||
return DynSock::Send(buf, len, flags);
|
||||
}
|
||||
}
|
||||
|
||||
mutable bool m_have_sent{false};
|
||||
};
|
||||
|
||||
// Simpler server startup than the last test
|
||||
CService addr_bind{Lookup("0.0.0.0", /*portDefault=*/0, /*fAllowLookup=*/false).value()};
|
||||
BOOST_REQUIRE(server.BindAndStartListening(addr_bind));
|
||||
server.StartSocketsThreads();
|
||||
|
||||
// Prepare initial requests
|
||||
int num_requests = 3;
|
||||
// Use keep-alive so the server holds the connection open for all requests.
|
||||
std::string keepalive_request{full_request};
|
||||
keepalive_request.replace(keepalive_request.find("Connection: close"), 17, "Connection: keep-alive");
|
||||
// Combine all requests so they are read from the socket on a single iteration of the I/O loop
|
||||
std::string all_requests;
|
||||
for (int i = 0; i < num_requests; i++) {
|
||||
all_requests += keepalive_request;
|
||||
}
|
||||
|
||||
// Watch the log messages to ensure that the first two replies were sent
|
||||
// together. This indicates the non-optimistic send path was used
|
||||
// because a reply was already sitting in the send buffer when a second reply
|
||||
// was added.
|
||||
DebugLogHelper find_two_replies{strprintf("Sent %d bytes to client", reply_length * 2),
|
||||
[&](const std::string* s) {
|
||||
return true;
|
||||
}};
|
||||
// Last reply should be sent on its own by optimistic send path, because
|
||||
// the send buffer was empty when the reply was written.
|
||||
DebugLogHelper find_one_reply{strprintf("Sent %d bytes to client", reply_length),
|
||||
[&](const std::string* s) {
|
||||
return true;
|
||||
}};
|
||||
|
||||
// Connect the ErrorSock as mock client with the preloaded data and get a handle on the I/O pipes
|
||||
std::shared_ptr<ErrorSock::Pipes> mock_client_socket_pipes{
|
||||
ConnectClient<ErrorSock>(std::as_bytes(std::span(all_requests)))
|
||||
};
|
||||
|
||||
// Wait up to one minute for the last reply from the server
|
||||
std::string actual;
|
||||
char buf[0x10000] = {};
|
||||
int attempts = 1000;
|
||||
while (attempts > 0)
|
||||
{
|
||||
ssize_t bytes_read = mock_client_socket_pipes->send.GetBytes(buf, sizeof(buf), 0);
|
||||
if (bytes_read > 0) {
|
||||
actual.append(buf, bytes_read);
|
||||
if (actual.find(strprintf("height: %d", num_requests - 1)) != std::string::npos) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::this_thread::sleep_for(10ms);
|
||||
--attempts;
|
||||
}
|
||||
|
||||
// All replies were received
|
||||
for (int i = 0; i < num_requests; i++) {
|
||||
BOOST_REQUIRE(actual.find(strprintf("height: %d", i)) != std::string::npos);
|
||||
}
|
||||
|
||||
// Close the keep-alive connection
|
||||
server.DisconnectAllClients();
|
||||
|
||||
server.InterruptNet();
|
||||
server.JoinSocketsThreads();
|
||||
server.StopListening();
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
|
||||
@@ -657,24 +657,6 @@ SocketTestingSetup::~SocketTestingSetup()
|
||||
CreateSock = m_create_sock_orig;
|
||||
}
|
||||
|
||||
std::shared_ptr<DynSock::Pipes> SocketTestingSetup::ConnectClient(std::span<const std::byte> data)
|
||||
{
|
||||
// I/O pipes for a mock Connected Socket we can read and write to.
|
||||
auto connected_socket_pipes(std::make_shared<DynSock::Pipes>());
|
||||
|
||||
// Insert the payload
|
||||
connected_socket_pipes->recv.PushBytes(data.data(), data.size());
|
||||
|
||||
// Create the Mock Connected Socket that represents a client.
|
||||
// It needs I/O pipes but its queue can remain empty
|
||||
std::unique_ptr<DynSock> connected_socket{std::make_unique<DynSock>(connected_socket_pipes)};
|
||||
|
||||
// Push into the queue of Accepted Sockets returned by the local CreateSock()
|
||||
m_accepted_sockets.Push(std::move(connected_socket));
|
||||
|
||||
return connected_socket_pipes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns a real block (0000000000013b8ab2cd513b0261a14096412195a72a0c4827d229dcc7e0f7af)
|
||||
* with 9 txs.
|
||||
|
||||
@@ -257,10 +257,18 @@ public:
|
||||
~SocketTestingSetup();
|
||||
|
||||
/**
|
||||
* Connect to the socket with a mock client (a DynSock) and send pre-loaded data.
|
||||
* Connect to the socket with a mock client and send pre-loaded data.
|
||||
* Returns the I/O pipes from the mock client so we can read response data sent to it.
|
||||
* Template parameter selects the socket type: DynSock by default.
|
||||
*/
|
||||
std::shared_ptr<DynSock::Pipes> ConnectClient(std::span<const std::byte> data);
|
||||
template <typename T = DynSock>
|
||||
std::shared_ptr<typename T::Pipes> ConnectClient(std::span<const std::byte> data)
|
||||
{
|
||||
auto connected_socket_pipes(std::make_shared<typename T::Pipes>());
|
||||
connected_socket_pipes->recv.PushBytes(data.data(), data.size());
|
||||
m_accepted_sockets.Push(std::make_unique<T>(connected_socket_pipes));
|
||||
return connected_socket_pipes;
|
||||
}
|
||||
|
||||
private:
|
||||
//! Save the original value of CreateSock here and restore it when the test ends.
|
||||
|
||||
Reference in New Issue
Block a user