flatten 20260225

This commit is contained in:
Timothy Prepscius
2026-02-25 12:39:24 -05:00
commit fa54be052a
315 changed files with 49791 additions and 0 deletions

View File

@@ -0,0 +1,985 @@
// TJP COPYRIGHT HEADER
#if defined(TJP_CORE_HEADER_ONLY)
#pragma once
#endif
#include <tjp/core/header_only/compile.h>
#include "Server.hpp"
#include "beast.hpp"
#include <tjp/core/containers/Map.h>
#include <tjp/core/containers/Vector.h>
#include <tjp/core/exception/Exception.hpp>
#include <tjp/core/future/Futures.hpp>
#include <atomic>
#include <deque>
#include <functional>
#include <iterator>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <cstdio>
namespace tjp::core::http::beast {
namespace net = boost::asio;
namespace beast_ = boost::beast;
namespace http_ = beast_::http;
using tcp = net::ip::tcp;
struct RequestAdapter :
http::Request,
std::enable_shared_from_this<RequestAdapter>
{
String methodStorage;
String targetStorage;
String bodyStorage;
Vector<Header> headerStorage;
RequestAdapter(const http_::request<http_::string_body> &in)
{
methodStorage = String(in.method_string());
targetStorage = String(in.target());
bodyStorage = in.body();
method = methodStorage;
target = targetStorage;
body = bodyStorage;
auto headerCount = static_cast<size_t>(std::distance(in.base().begin(), in.base().end()));
headerStorage.reserve(headerCount);
headers.reserve(headerCount);
for (const auto &field: in.base())
{
headerStorage.emplace_back(Header {
String(field.name_string()),
String(field.value())
});
auto &header = headerStorage.back();
headers.emplace_back(HeaderView {
header.key,
header.value
});
}
}
} ;
using StreamState = http::PipeState;
struct ResponseAdapter :
http::Response,
std::enable_shared_from_this<ResponseAdapter>
{
String body;
bool deferred = false;
std::shared_ptr<RequestAdapter> requestHolder;
std::shared_ptr<StreamState> streamState;
std::shared_ptr<core::Function<void(std::shared_ptr<ResponseAdapter>)>> onReady;
void notifyReady()
{
if (onReady && *onReady)
(*onReady)(shared_from_this());
}
void flushHeaders(const StringView &) override
{
}
void write(const StringView &data) override
{
body = String(data);
}
std::shared_ptr<http::DataSink> stream() override
{
Optional<size_t> size;
for (const auto &header: headers)
{
if (header.key != "Content-Length")
continue;
try
{
size = static_cast<size_t>(std::stoull(String(header.value)));
}
catch (...)
{
// ignore malformed values and keep scanning
}
}
deferred = true;
streamState = StreamState::make(size);
return streamState->sink();
}
http_::response<http_::string_body> toBeast(
unsigned version,
bool keepAlive
)
{
http_::response<http_::string_body> out {
static_cast<http_::status>(status),
version
};
std::optional<String> contentType;
std::optional<String> contentLength;
out.keep_alive(keepAlive);
for (auto &header: headers)
{
if (header.key == "Content-Length")
{
contentLength = header.value;
continue;
}
if (header.key == "Content-Type")
{
contentType = header.value;
continue;
}
out.set(String(header.key), String(header.value));
}
if (!contentType)
contentType = "text/plain";
if (contentType)
out.set(http_::field::content_type, *contentType);
out.body() = body;
if (contentLength)
out.set(http_::field::content_length, *contentLength);
else
out.prepare_payload();
return out;
}
} ;
static Optional<size_t> readContentLengthFromHeaders(const Vector<Header> &headers)
{
for (const auto &header: headers)
{
if (header.key != "Content-Length")
continue;
try
{
return static_cast<size_t>(std::stoull(String(header.value)));
}
catch (...)
{
// Ignore malformed content-length and keep scanning.
}
}
return {};
}
static void applyResult(ResponseAdapter &response, Result result)
{
response.status = result.status.value_or(200);
for (auto &[key, value]: result.headers)
response.header(key, value);
if (result.type)
response.header("Content-Type", *result.type);
auto streamSize = result.streamSize;
if (!streamSize)
streamSize = readContentLengthFromHeaders(response.headers);
if (streamSize && !response.hasHeader("Content-Length"))
response.content_length(*streamSize);
if (result.streamOwner)
{
response.deferred = true;
response.streamState = std::move(result.streamOwner);
return;
}
if (result.stream)
{
response.status = 500;
response.header("Content-Type", "text/plain");
response.write("Invalid stream owner");
return;
}
response.write(result.body.value_or(String {}));
}
struct Server::I
{
struct Route
{
Method method;
Matcher matcher;
FutureHandler handler;
} ;
struct Session;
struct Listener;
net::io_context io;
std::unique_ptr<tcp::acceptor> acceptor;
std::shared_ptr<Listener> listener;
using WorkGuard = net::executor_work_guard<net::io_context::executor_type>;
std::unique_ptr<WorkGuard> workGuard;
std::mutex routesMutex;
Vector<Route> routes;
Vector<StrongPtr<Component>> components;
Futures futures;
std::vector<std::thread> threads;
std::optional<size_t> numThreads;
std::atomic<bool> running = false;
std::atomic<bool> listening = false;
size_t threadCount() const
{
if (numThreads && *numThreads > 0)
return *numThreads;
auto n = std::thread::hardware_concurrency();
return n == 0 ? 1 : n;
}
bool findHandler(
Request &request,
FutureHandler &out
)
{
std::lock_guard<std::mutex> lock(routesMutex);
for (const auto &route: routes)
{
if (route.method != request.method)
continue;
if (route.matcher)
if (auto matches = route.matcher(request.target))
{
request.matches = *matches;
out = route.handler;
return true;
}
}
return false;
}
};
struct Server::I::Session : std::enable_shared_from_this<Session>
{
I *i;
beast_::tcp_stream stream;
beast_::flat_buffer buffer;
http_::request<http_::string_body> request;
std::shared_ptr<ResponseAdapter> activeStreamResponse;
std::optional<http_::response<http_::empty_body>> activeStreamHeaderMessage;
std::optional<http_::response_serializer<http_::empty_body>> activeStreamHeader;
std::shared_ptr<String> activeStreamPayload;
std::shared_ptr<String> activeStreamChunkWire;
size_t activeStreamBytesWritten = 0;
bool activeStreamHeaderSent = false;
bool activeStreamWriteInFlight = false;
bool activeStreamChunked = false;
bool activeStreamHeadOnly = false;
Session(I *i_, tcp::socket &&socket) :
i(i_),
stream(std::move(socket))
{}
void run()
{
doRead();
}
void doRead()
{
request = {};
http_::async_read(
stream,
buffer,
request,
[self = shared_from_this()](beast_::error_code ec, size_t bytes) {
self->onRead(ec, bytes);
}
);
}
void onRead(beast_::error_code ec, size_t)
{
if (ec == http_::error::end_of_stream)
{
doClose();
return;
}
if (ec)
{
doClose();
return;
}
handleRequest();
}
void handleRequest()
{
auto request_ = std::make_shared<RequestAdapter>(request);
auto response = std::make_shared<ResponseAdapter>();
response->requestHolder = request_;
response->onReady = std::make_shared<core::Function<void(std::shared_ptr<ResponseAdapter>)>>(
[self = shared_from_this()](std::shared_ptr<ResponseAdapter> response_) {
net::dispatch(
self->stream.get_executor(),
[self, response_]() {
if (response_->streamState)
self->startStreaming(response_);
else
self->send(response_->toBeast(self->request.version(), self->request.keep_alive()));
}
);
}
);
try
{
FutureHandler handler;
if (i->findHandler(*request_, handler))
{
response->deferred = true;
auto future = handler(*request_).then([response, request_](auto &&future_) {
(void)request_;
try
{
applyResult(*response, future_.get());
}
catch (Result &result)
{
applyResult(*response, Result(result));
}
catch (Exception &e)
{
response->status = 400;
response->header("Content-Type", "text/plain");
response->write("Error: " + e.what());
}
catch (...)
{
response->status = 400;
response->header("Content-Type", "text/plain");
response->write("Error");
}
response->notifyReady();
});
i->futures.add(future);
}
else
{
response->status = 404;
response->header("Content-Type", "text/plain");
response->write("Not found");
response->notifyReady();
}
}
catch (Result &result)
{
applyResult(*response, Result(result));
response->notifyReady();
}
catch (Exception &e)
{
response->status = 400;
response->header("Content-Type", "text/plain");
response->write("Error: " + e.what());
response->notifyReady();
}
catch (...)
{
response->status = 400;
response->header("Content-Type", "text/plain");
response->write("Error");
response->notifyReady();
}
}
void send(http_::response<http_::string_body> &&response)
{
auto keepAlive = response.keep_alive();
auto sharedResponse = std::make_shared<http_::response<http_::string_body>>(std::move(response));
http_::async_write(
stream,
*sharedResponse,
[self = shared_from_this(), sharedResponse, keepAlive](beast_::error_code ec, size_t bytes) {
self->onWrite(keepAlive, ec, bytes);
}
);
}
void onWrite(bool keepAlive, beast_::error_code ec, size_t)
{
if (ec)
{
doClose();
return;
}
if (!keepAlive)
{
doClose();
return;
}
doRead();
}
void clearStreamingState()
{
activeStreamResponse.reset();
activeStreamHeaderMessage.reset();
activeStreamHeader.reset();
activeStreamPayload.reset();
activeStreamChunkWire.reset();
activeStreamBytesWritten = 0;
activeStreamHeaderSent = false;
activeStreamWriteInFlight = false;
activeStreamChunked = false;
activeStreamHeadOnly = false;
}
void startStreaming(const std::shared_ptr<ResponseAdapter> &response)
{
if (!response || !response->streamState)
return;
if (activeStreamResponse)
return;
activeStreamResponse = response;
activeStreamHeaderSent = false;
activeStreamWriteInFlight = false;
activeStreamHeaderMessage.reset();
activeStreamHeader.reset();
activeStreamPayload.reset();
activeStreamChunkWire.reset();
activeStreamBytesWritten = 0;
activeStreamHeadOnly = request.method() == http_::verb::head;
activeStreamChunked = !response->streamState->size().has_value() && !activeStreamHeadOnly;
response->streamState->setWake([self = shared_from_this()]() {
net::dispatch(
self->stream.get_executor(),
[self]() {
self->pumpStreaming();
}
);
});
pumpStreaming();
}
void pumpStreaming()
{
if (!activeStreamResponse || !activeStreamResponse->streamState)
return;
if (activeStreamWriteInFlight)
return;
auto response = activeStreamResponse;
auto state = response->streamState;
auto knownSize = state->size();
if (!activeStreamHeaderSent)
{
http_::response<http_::empty_body> header {
static_cast<http_::status>(response->status),
request.version()
};
header.keep_alive(request.keep_alive());
if (knownSize)
header.content_length(*knownSize);
else
if (activeStreamChunked)
header.chunked(true);
std::optional<String> contentType;
for (auto &header_: response->headers)
{
if (header_.key == "Content-Length")
continue;
if (header_.key == "Transfer-Encoding")
continue;
if (header_.key == "Content-Type")
{
contentType = header_.value;
continue;
}
header.set(String(header_.key), String(header_.value));
}
if (!contentType)
contentType = "text/plain";
if (contentType)
header.set(http_::field::content_type, *contentType);
activeStreamHeaderMessage.emplace(std::move(header));
activeStreamHeader.emplace(*activeStreamHeaderMessage);
activeStreamWriteInFlight = true;
http_::async_write_header(
stream,
*activeStreamHeader,
[self = shared_from_this()](beast_::error_code ec, size_t) {
self->onStreamHeaderWrite(ec);
}
);
return;
}
if (activeStreamHeadOnly)
{
auto keepAlive = request.keep_alive();
clearStreamingState();
if (keepAlive)
doRead();
else
doClose();
return;
}
if (!activeStreamChunked)
{
if (!knownSize)
{
clearStreamingState();
doClose();
return;
}
if (activeStreamBytesWritten >= *knownSize)
{
auto keepAlive = request.keep_alive();
clearStreamingState();
if (keepAlive)
doRead();
else
doClose();
return;
}
auto chunk = state->popChunk();
if (chunk)
{
auto remaining = *knownSize - activeStreamBytesWritten;
if (chunk->size() > remaining)
chunk->resize(remaining);
activeStreamPayload = chunk;
activeStreamWriteInFlight = true;
net::async_write(
stream,
net::buffer(*activeStreamPayload),
[self = shared_from_this(), payload = activeStreamPayload](beast_::error_code ec, size_t bytes) {
(void)payload;
self->onStreamChunkWrite(ec, bytes);
}
);
return;
}
if (state->isEnded())
{
// Producer ended early without delivering promised size.
clearStreamingState();
doClose();
}
return;
}
auto chunk = state->popChunk();
if (chunk)
{
char sizeHex[32] = {};
auto length = std::snprintf(sizeHex, sizeof(sizeHex), "%zx", chunk->size());
if (length < 0)
{
clearStreamingState();
doClose();
return;
}
activeStreamChunkWire = std::make_shared<String>();
activeStreamChunkWire->append(sizeHex, static_cast<size_t>(length));
activeStreamChunkWire->append("\r\n", 2);
activeStreamChunkWire->append(*chunk);
activeStreamChunkWire->append("\r\n", 2);
activeStreamWriteInFlight = true;
net::async_write(
stream,
net::buffer(*activeStreamChunkWire),
[self = shared_from_this(), chunkWire = activeStreamChunkWire](beast_::error_code ec, size_t) {
(void)chunkWire;
self->onStreamChunkedWrite(ec);
}
);
return;
}
if (state->isEnded())
{
activeStreamChunkWire = std::make_shared<String>("0\r\n\r\n");
activeStreamWriteInFlight = true;
net::async_write(
stream,
net::buffer(*activeStreamChunkWire),
[self = shared_from_this(), chunkWire = activeStreamChunkWire](beast_::error_code ec, size_t) {
(void)chunkWire;
self->onStreamChunkedDoneWrite(ec);
}
);
}
}
void onStreamHeaderWrite(beast_::error_code ec)
{
activeStreamWriteInFlight = false;
activeStreamHeaderSent = true;
activeStreamHeaderMessage.reset();
activeStreamHeader.reset();
if (ec)
{
clearStreamingState();
doClose();
return;
}
pumpStreaming();
}
void onStreamChunkWrite(beast_::error_code ec, size_t bytes)
{
activeStreamWriteInFlight = false;
activeStreamBytesWritten += bytes;
activeStreamPayload.reset();
if (ec)
{
clearStreamingState();
doClose();
return;
}
pumpStreaming();
}
void onStreamChunkedWrite(beast_::error_code ec)
{
activeStreamWriteInFlight = false;
activeStreamChunkWire.reset();
if (ec)
{
clearStreamingState();
doClose();
return;
}
pumpStreaming();
}
void onStreamChunkedDoneWrite(beast_::error_code ec)
{
activeStreamWriteInFlight = false;
activeStreamChunkWire.reset();
if (ec)
{
clearStreamingState();
doClose();
return;
}
auto keepAlive = request.keep_alive();
clearStreamingState();
if (keepAlive)
doRead();
else
doClose();
}
void doClose()
{
clearStreamingState();
beast_::error_code ec;
stream.socket().shutdown(tcp::socket::shutdown_send, ec);
stream.socket().close(ec);
}
};
struct Server::I::Listener : std::enable_shared_from_this<Listener>
{
I *i;
Listener(I *i_) :
i(i_)
{}
void run()
{
doAccept();
}
void doAccept()
{
if (!i->acceptor || !i->running)
return;
i->acceptor->async_accept(
net::make_strand(i->io),
[self = shared_from_this()](beast_::error_code ec, tcp::socket socket) {
self->onAccept(ec, std::move(socket));
}
);
}
void onAccept(beast_::error_code ec, tcp::socket socket)
{
if (!i->running)
return;
if (!ec)
std::make_shared<Session>(i, std::move(socket))->run();
else
if (ec == net::error::operation_aborted)
return;
doAccept();
}
};
static tcp::endpoint toEndpoint(const String &bindTo, int port)
{
beast_::error_code ec;
auto address = net::ip::make_address(bindTo, ec);
if (ec)
throw Exception { "Invalid bind address" };
if (port < 0 || port > 65535)
throw Exception { "Invalid port" };
return tcp::endpoint {
address,
static_cast<unsigned short>(port)
};
}
static void throwIfError(beast_::error_code ec, const char *message)
{
if (ec)
throw Exception { message };
}
TJP_CORE_HEADER_ONLY_INLINE
Server::Server()
{
i = strong<I>();
}
TJP_CORE_HEADER_ONLY_INLINE
Server::~Server()
{
stop();
}
TJP_CORE_HEADER_ONLY_INLINE
void Server::add(const StrongPtr<Component> &component, const String &urlPrefix)
{
i->components.emplace_back(component);
component->use(*this, urlPrefix);
}
TJP_CORE_HEADER_ONLY_INLINE
void Server::setNumThreads(size_t n)
{
i->numThreads = n == 0 ? 1 : n;
}
TJP_CORE_HEADER_ONLY_INLINE
void Server::start(const String &bindTo, int port)
{
if (i->running.exchange(true))
throw Exception { "Webserver already running" };
try
{
i->io.restart();
i->acceptor = std::make_unique<tcp::acceptor>(i->io);
i->workGuard = std::make_unique<I::WorkGuard>(i->io.get_executor());
auto endpoint = toEndpoint(bindTo, port);
beast_::error_code ec;
i->acceptor->open(endpoint.protocol(), ec);
throwIfError(ec, "Could not open socket");
i->acceptor->set_option(net::socket_base::reuse_address(true), ec);
throwIfError(ec, "Could not set socket options");
i->acceptor->bind(endpoint, ec);
throwIfError(ec, "Could not bind to endpoint");
i->acceptor->listen(net::socket_base::max_listen_connections, ec);
throwIfError(ec, "Could not listen");
i->listener = std::make_shared<I::Listener>(i.get());
i->listener->run();
i->listening = true;
auto count = i->threadCount();
i->threads.reserve(count);
for (size_t idx = 0; idx < count; ++idx)
{
i->threads.emplace_back([state = i.get()]() {
state->io.run();
});
}
}
catch (...)
{
stop();
throw;
}
}
TJP_CORE_HEADER_ONLY_INLINE
void Server::run(const String &bindTo, int port)
{
if (i->running.exchange(true))
throw Exception { "Webserver already running" };
try
{
i->io.restart();
i->acceptor = std::make_unique<tcp::acceptor>(i->io);
i->workGuard = std::make_unique<I::WorkGuard>(i->io.get_executor());
auto endpoint = toEndpoint(bindTo, port);
beast_::error_code ec;
i->acceptor->open(endpoint.protocol(), ec);
throwIfError(ec, "Could not open socket");
i->acceptor->set_option(net::socket_base::reuse_address(true), ec);
throwIfError(ec, "Could not set socket options");
i->acceptor->bind(endpoint, ec);
throwIfError(ec, "Could not bind to endpoint");
i->acceptor->listen(net::socket_base::max_listen_connections, ec);
throwIfError(ec, "Could not listen");
i->listener = std::make_shared<I::Listener>(i.get());
i->listener->run();
i->listening = true;
auto count = i->threadCount();
if (count > 1)
{
i->threads.reserve(count - 1);
for (size_t idx = 1; idx < count; ++idx)
{
i->threads.emplace_back([state = i.get()]() {
state->io.run();
});
}
}
i->io.run();
for (auto &thread: i->threads)
if (thread.joinable())
thread.join();
i->threads.clear();
i->listener.reset();
i->acceptor.reset();
i->workGuard.reset();
i->listening = false;
i->running = false;
}
catch (...)
{
stop();
throw;
}
}
TJP_CORE_HEADER_ONLY_INLINE
void Server::stop()
{
if (!i->running.exchange(false))
return;
i->listening = false;
if (i->acceptor)
{
beast_::error_code ec;
i->acceptor->cancel(ec);
i->acceptor->close(ec);
}
i->listener.reset();
i->workGuard.reset();
i->io.stop();
for (auto &thread: i->threads)
if (thread.joinable())
thread.join();
i->threads.clear();
i->acceptor.reset();
i->io.restart();
}
TJP_CORE_HEADER_ONLY_INLINE
bool Server::isRunning()
{
return i->running && i->listening;
}
TJP_CORE_HEADER_ONLY_INLINE
void Server::bind(const Method &method, Matcher &&matcher, FutureHandler &&handler)
{
if (!matcher)
throw Exception { "Invalid route matcher" };
std::lock_guard<std::mutex> lock(i->routesMutex);
i->routes.emplace_back(I::Route {
method,
std::move(matcher),
std::move(handler)
});
}
} // namespace

View File

@@ -0,0 +1,9 @@
// TJP COPYRIGHT HEADER
#pragma once
namespace tjp::core::http::beast {
struct Server;
} // namespace

View File

@@ -0,0 +1,50 @@
// TJP COPYRIGHT HEADER
#pragma once
#include "Server.h"
#include "../Server.hpp"
#include "../Component.hpp"
#include <tjp/core/header_only/compile.h>
#include <tjp/core/ptr/Ptr.h>
#include <tjp/core/string/String.h>
namespace tjp::core::http::beast {
struct Server : http::Server
{
using Super = http::Server;
using Super::bind;
struct I;
StrongPtr<I> i;
Server();
~Server();
void add(
const StrongPtr<Component> &component,
const String &urlPrefix = {}
) override;
void setNumThreads(size_t) override;
void start(const String &bindTo, int port) override;
void run(const String &bindTo, int port) override;
void stop() override;
bool isRunning() override;
void bind(
const Method &method,
Matcher &&matcher,
FutureHandler &&
) override;
};
} // namespace
#ifdef TJP_CORE_HEADER_ONLY
#include "Server.cpp"
#endif

View File

@@ -0,0 +1,505 @@
// TJP COPYRIGHT HEADER
#include "../Server.hpp"
#include "../../hirose/httplib.h"
#include <tjp/core/testing/catch.hpp>
#include <tjp/core/testing/wait_until.h>
#include <tjp/core/hash/Hash.h>
#include <tjp/core/types/Types.h>
#include <algorithm>
#include <string>
#include <thread>
namespace tjp::core::http::beast {
SCENARIO("tjp::core::http::beast")
{
GIVEN("server")
{
Server server;
server.setNumThreads(2);
auto make_large_payload = []() {
String data;
data.reserve(64 * 1024);
for (size_t i = 0; i < (64 * 1024); ++i)
data.push_back(static_cast<char>('a' + (i % 26)));
return data;
};
auto large_payload = make_large_payload();
server.bind_with("GET", "/hello", [](const Request &) -> Result {
Result result;
result.type = "text/plain";
result.body = "hello";
return result;
});
server.bind_with("POST", "/echo", [](const Request &req) -> Result {
Result result;
result.status = 201;
result.type = "text/plain";
result.body = String(req.body);
return result;
});
server.bind_with("PUT", "/replace", [](const Request &req) -> Result {
Result result;
result.type = "text/plain";
result.body = "updated:" + String(req.body);
return result;
});
server.bind_with("HEAD", "/meta", [](const Request &) -> Result {
Result result;
result.headers.push_back(Header { "X-Test-Head", "ok" });
return result;
});
server.bind_with("GET", "/empty", [](const Request &) -> Result {
Result result;
result.type = "text/plain";
result.body = "";
return result;
});
server.bind_with("HEAD", "/empty", [](const Request &) -> Result {
Result result;
result.type = "text/plain";
result.headers.push_back(Header { "Content-Length", "0" });
return result;
});
server.bind_with("HEAD", "/head-sized", [](const Request &) -> Result {
Result result;
result.type = "text/plain";
result.headers.push_back(Header { "Content-Length", "6"} );
return result;
});
server.bind_with("GET", "/range", [](const Request &req) -> Result {
Result result;
String full = "0123456789";
auto range = req.header("Range");
if (range && *range == "bytes=2-5")
{
result.status = 206;
result.headers.push_back(Header {"Content-Range", "bytes 2-5/10"});
result.type = "text/plain";
result.body = "2345";
return result;
}
result.type = "text/plain";
result.body = full;
return result;
});
server.bind_with("GET", match_exact("/match/exact"), [](const Request &) -> Result {
Result result;
result.type = "text/plain";
result.body = "exact";
return result;
});
server.bind_with("GET", match_prefix("/match/prefix"), [](const Request &req) -> Result {
Result result;
result.type = "text/plain";
result.body = "prefix:" + String(req.target);
return result;
});
server.bind_with("GET", match_regex("^/match/regex/[0-9]+$"), [](const Request &) -> Result {
Result result;
result.type = "text/plain";
result.body = "regex";
return result;
});
server.bind_with(
"GET",
[](StringView url) -> Optional<Matches>
{ return url == "/match/lambda" ? Matches() : Optional<Matches>();
},
[](const Request &) -> Result {
Result result;
result.type = "text/plain";
result.body = "lambda";
return result;
}
);
server.bind_with("GET", "/stream", [](const Request &) -> Result {
Result result;
const String expected = "streamed-body";
result.type = "text/plain";
auto out = result.stream_body(expected.size());
std::this_thread::sleep_for(std::chrono::milliseconds(10));
out->write(expected.data(), expected.size());
out->done();
return result;
});
server.bind_with("GET", "/stream-signal", [](const Request &) -> Result {
Result result;
result.type = "text/plain";
auto out = result.stream_body(6);
std::thread([out]() {
std::this_thread::sleep_for(std::chrono::milliseconds(20));
out->write("sig", 3);
std::this_thread::sleep_for(std::chrono::milliseconds(20));
out->write("nal", 3);
out->done();
}).detach();
return result;
});
server.bind_with("GET", "/stream-empty", [](const Request &) -> Result {
Result result;
result.type = "text/plain";
auto out = result.stream_body(0);
out->done();
return result;
});
server.bind_with("GET", "/stream-unknown", [](const Request &) -> Result {
Result result;
result.type = "text/plain";
auto out = result.stream_body();
std::thread([out]() {
std::this_thread::sleep_for(std::chrono::milliseconds(20));
out->write("sig", 3);
std::this_thread::sleep_for(std::chrono::milliseconds(20));
out->write("nal", 3);
out->done();
}).detach();
return result;
});
server.bind_with("HEAD", "/stream-head-known", [](const Request &) -> Result {
Result result;
result.type = "text/plain";
auto out = result.stream_body(6);
out->write("ABCDEF", 6);
out->done();
return result;
});
server.bind_with("HEAD", "/stream-head-unknown", [](const Request &) -> Result {
Result result;
result.type = "text/plain";
auto out = result.stream_body();
out->write("ignored", 7);
out->done();
return result;
});
server.bind_with("GET", "/stream-large", [large_payload](const Request &) -> Result {
Result result;
result.type = "text/plain";
auto out = result.stream_body(large_payload.size());
const size_t chunk = 8192;
for (size_t offset = 0; offset < large_payload.size(); offset += chunk)
{
auto size = std::min(chunk, large_payload.size() - offset);
out->write(large_payload.data() + offset, size);
}
out->done();
return result;
});
server.bind_with("GET", "/future", [](const Request &) -> Result {
std::this_thread::sleep_for(std::chrono::milliseconds(25));
Result result;
result.status = 202;
result.type = "text/plain";
result.body = "future-ok";
return result;
});
server.bind_with("GET", "/future-stream-known", [](const Request &) -> Result {
Result result;
result.status = 200;
result.type = "text/plain";
auto out = result.stream_body(6);
std::thread([out]() {
std::this_thread::sleep_for(std::chrono::milliseconds(15));
out->write("sig", 3);
std::this_thread::sleep_for(std::chrono::milliseconds(15));
out->write("nal", 3);
out->done();
}).detach();
return result;
});
server.bind("GET", "/future-stream-unknown", FutureHandler([](const Request &) {
Result result;
result.status = 200;
result.type = "text/plain";
auto out = result.stream_body();
std::thread([out]() {
std::this_thread::sleep_for(std::chrono::milliseconds(15));
out->write("sig", 3);
std::this_thread::sleep_for(std::chrono::milliseconds(15));
out->write("nal", 3);
out->done();
}).detach();
return future_with([result]() {
return result;
});
}));
auto before = std::chrono::steady_clock::now();
server.start("127.0.0.1", 4792);
auto after = std::chrono::steady_clock::now();
REQUIRE((after - before) < std::chrono::seconds(1));
core::wait_until(std::chrono::seconds(10), [&]() { return server.isRunning(); });
WHEN("a get is made")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Get("/hello");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body == "hello");
}
WHEN("a post is made")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Post("/echo", "posted-body", "text/plain");
REQUIRE(bool(contents));
REQUIRE(contents->status == 201);
REQUIRE(contents->body == "posted-body");
}
WHEN("a put is made")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Put("/replace", "payload", "text/plain");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body == "updated:payload");
}
WHEN("a head is made")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Head("/meta");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->get_header_value("X-Test-Head") == "ok");
REQUIRE(contents->body.empty());
}
WHEN("a get of empty content is made")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Get("/empty");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body.empty());
REQUIRE(contents->get_header_value("Content-Type") == "text/plain");
REQUIRE(contents->get_header_value("Content-Length") == "0");
}
WHEN("a head of empty content is made")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Head("/empty");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body.empty());
REQUIRE(contents->get_header_value("Content-Length") == "0");
}
WHEN("a head of non-empty content is made")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Head("/head-sized");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body.empty());
REQUIRE(contents->get_header_value("Content-Type") == "text/plain");
REQUIRE(contents->get_header_value("Content-Length") == "6");
}
WHEN("a range request is made")
{
httplib::Client client("http://127.0.0.1:4792");
httplib::Headers headers;
headers.emplace("Range", "bytes=2-5");
auto contents = client.Get("/range", headers);
REQUIRE(bool(contents));
REQUIRE(contents->status == 206);
REQUIRE(contents->get_header_value("Content-Range") == "bytes 2-5/10");
REQUIRE(contents->body == "2345");
}
WHEN("an exact matcher route is requested")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Get("/match/exact");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body == "exact");
}
WHEN("a prefix matcher route is requested")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Get("/match/prefix/child");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body == "prefix:/match/prefix/child");
}
WHEN("a regex matcher route is requested")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Get("/match/regex/42");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body == "regex");
}
WHEN("a lambda matcher route is requested")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Get("/match/lambda");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body == "lambda");
}
WHEN("an unknown route is requested")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Get("/missing");
REQUIRE(bool(contents));
REQUIRE(contents->status == 404);
}
WHEN("a future backed get is made")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Get("/future");
REQUIRE(bool(contents));
REQUIRE(contents->status == 202);
REQUIRE(contents->body == "future-ok");
}
WHEN("a future-backed known-size stream get is made")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Get("/future-stream-known");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body == "signal");
REQUIRE(contents->get_header_value("Content-Length") == "6");
REQUIRE(contents->get_header_value("Transfer-Encoding").empty());
}
WHEN("a future-backed unknown-size stream get is made")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Get("/future-stream-unknown");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body == "signal");
REQUIRE(contents->get_header_value("Content-Length").empty());
REQUIRE(contents->get_header_value("Transfer-Encoding") == "chunked");
}
WHEN("a stream backed get is made")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Get("/stream");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body == "streamed-body");
REQUIRE(contents->get_header_value("Content-Type") == "text/plain");
REQUIRE(contents->get_header_value("Content-Length") == "13");
REQUIRE(contents->get_header_value("Transfer-Encoding").empty());
}
WHEN("a signal-backed stream get is made")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Get("/stream-signal");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body == "signal");
REQUIRE(contents->get_header_value("Content-Length") == "6");
REQUIRE(contents->get_header_value("Transfer-Encoding").empty());
}
WHEN("a zero-sized stream get is made")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Get("/stream-empty");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body.empty());
REQUIRE(contents->get_header_value("Content-Type") == "text/plain");
REQUIRE(contents->get_header_value("Content-Length") == "0");
REQUIRE(contents->get_header_value("Transfer-Encoding").empty());
}
WHEN("an unknown-sized stream get is made")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Get("/stream-unknown");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body == "signal");
REQUIRE(contents->get_header_value("Content-Length").empty());
REQUIRE(contents->get_header_value("Transfer-Encoding") == "chunked");
}
WHEN("a head of known-sized stream is made")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Head("/stream-head-known");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body.empty());
REQUIRE(contents->get_header_value("Content-Length") == "6");
REQUIRE(contents->get_header_value("Transfer-Encoding").empty());
}
WHEN("a head of unknown-sized stream is made")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Head("/stream-head-unknown");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body.empty());
REQUIRE(contents->get_header_value("Content-Length").empty());
REQUIRE(contents->get_header_value("Transfer-Encoding").empty());
}
WHEN("a large stream get is made")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Get("/stream-large");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body.size() > 32 * 1024);
REQUIRE(contents->body.size() == large_payload.size());
auto body_hash = hash_of<u64>(contents->body);
auto expected_hash = hash_of<u64>(large_payload);
REQUIRE(body_hash == expected_hash);
REQUIRE(contents->get_header_value("Content-Length") == std::to_string(large_payload.size()));
REQUIRE(contents->get_header_value("Transfer-Encoding").empty());
}
server.stop();
}
}
} // namespace

View File

@@ -0,0 +1,25 @@
// TJP COPYRIGHT HEADER
#if defined __GNUC__
#pragma GCC diagnostic push
//#pragma GCC diagnostic ignored "-Wcomma"
//#pragma GCC diagnostic ignored "-Wdocumentation"
//#pragma GCC diagnostic ignored "-Wshorten-64-to-32"
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
//#pragma GCC diagnostic ignored "-W#pragma-messages"
#pragma clang diagnostic ignored "-Wcomma"
#pragma clang diagnostic ignored "-Wdocumentation"
#pragma clang diagnostic ignored "-Wshorten-64-to-32"
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#pragma clang diagnostic ignored "-W#pragma-messages"
#pragma clang diagnostic ignored "-Wdocumentation"
#endif
#include <boost/asio.hpp>
#include <boost/beast.hpp>
#if defined __GNUC__
#pragma GCC diagnostic pop
#endif