Some basic context: I've homebrewed a C++ Rest Server using the boost.beast library, and I'm writing a Web App using typescript in Node.JS that calls this server.
So far, the server has worked just fine with basic http GET requests to the server, but I've run into problems when using POST requests. Specifically, my server is interpreting POST requests as instead being the "OPTIONS" verb.
My question is, why is this happening? Is this a mistake in my server implementation, or is it a strange quirk of my Web App?
Relevant Code:
RestServer.h
#pragma once
#include<memory>
#include<thread>
#include<functional>
#include<string>
#include<vector>
#include<unordered_map>
#include<string_view>
namespace net {
struct HTTPHeaders {
std::string target;
std::unordered_map<std::string, std::string> httpHeaders;
std::unordered_map<std::string, std::string> httpParameters;
};
using GETFunc = std::function<std::string(HTTPHeaders const&)>;
using POSTFunc = std::function<std::string(HTTPHeaders const&, std::string)>;
class RestServerImpl;
class RestServer {
std::unique_ptr<RestServerImpl> impl;
public:
RestServer(std::string_view name, uint16_t port, uint32_t maxThreadCount = std::thread::hardware_concurrency());
void start(std::unordered_map<std::string, GETFunc> getFunctions, std::unordered_map<std::string, POSTFunc> postFunctions);
RestServer(RestServer const&) = delete;
~RestServer();
};
}
RestServer.cpp
#include<Network/RestServer.h>
#include<atomic>
#include<boost/beast.hpp>
#include<iostream>
#include <boost/beast/version.hpp>
#include <boost/asio/dispatch.hpp>
#include <boost/asio/strand.hpp>
#include<mutex>
namespace net {
namespace networking = boost::asio;
namespace beast = boost::beast;
namespace http = beast::http;
using tcp = networking::ip::tcp;
namespace {
void workFunc(std::atomic_bool& shouldStop, networking::io_context& ioContext) {
while (!shouldStop) {
ioContext.run();
}
}
}
class RestServerImpl {
friend class RestServer;
friend class RestServerSession;
struct CaseInsensitiveStringComparator {
bool operator()(std::string const& a, std::string const& b) const {
return std::equal(a.begin(), a.end(), b.begin(), b.end(), [](char a, char b) {return std::toupper(a) == std::toupper(b); });
}
bool operator()(std::string_view a, std::string const& b) const {
return std::equal(a.begin(), a.end(), b.begin(), b.end(), [](char a, char b) {return std::toupper(a) == std::toupper(b); });
}
bool operator()(std::string const& a, std::string_view b) const {
return std::equal(a.begin(), a.end(), b.begin(), b.end(), [](char a, char b) {return std::toupper(a) == std::toupper(b); });
}
size_t operator()(std::string a) const {
for (char& c : a)
c = std::toupper(c);
return std::hash<std::string>{}(a);
}
size_t operator()(std::string_view str) const {
std::string a{ str };
for (char& c : a)
c = std::toupper(c);
return std::hash<std::string>{}(a);
}
};
std::vector<std::thread> threads;
std::unordered_map<std::string, GETFunc, CaseInsensitiveStringComparator, CaseInsensitiveStringComparator> getFunctions;
std::unordered_map<std::string, POSTFunc, CaseInsensitiveStringComparator, CaseInsensitiveStringComparator> postFunctions;
std::atomic_bool shouldStop;
networking::io_context ioContext;
tcp::acceptor acceptor;
std::mutex funcMutex;
class RestServerSession : public std::enable_shared_from_this<RestServerSession> {
friend RestServerImpl;
RestServerImpl* parent;
beast::tcp_stream stream;
beast::flat_buffer buffer;
using string_request = http::request<http::string_body>;
using string_response = http::response<http::string_body>;
string_request request;
string_response response;
void do_read() {
stream.expires_after(std::chrono::seconds(30));
http::async_read(
stream,
buffer,
request,
[ptr = shared_from_this()](beast::error_code ec, size_t bytes_transferred) {
ptr->handle_read(ec, bytes_transferred);
}
);
}
void do_close() {
stream.socket().shutdown(tcp::socket::shutdown_send);
}
void handle_read(beast::error_code ec, size_t bytes_transferred) {
if (ec == http::error::end_of_stream) {
do_close();
return;
}
if (ec) {
std::cerr << "Problem reading from Socket: " << ec.what() << std::endl;
return;
}
handle_request(std::move(request));
}
void handle_request(string_request request) {
std::cout << "Handling Request" << std::endl;
// Returns a bad request response
auto const bad_request =
[&request](beast::string_view why)
{
string_response response{ http::status::bad_request, request.version() };
response.set(http::field::server, BOOST_BEAST_VERSION_STRING);
response.set(http::field::content_type, "text/html");
response.set(http::field::access_control_allow_origin, "*");
response.keep_alive(request.keep_alive());
response.body() = std::string(why);
response.content_length(response.body().size());
response.prepare_payload();
return response;
};
// Returns a not found response
auto const not_found =
[&request](beast::string_view target)
{
string_response response{ http::status::not_found, request.version() };
response.set(http::field::server, BOOST_BEAST_VERSION_STRING);
response.set(http::field::content_type, "text/html");
response.set(http::field::access_control_allow_origin, "*");
response.keep_alive(request.keep_alive());
response.body() = "The resource '" + std::string(target) + "' was not found.";
response.content_length(response.body().size());
response.prepare_payload();
return response;
};
string_response response{ http::status::ok, request.version() };
response.set(http::field::server, BOOST_BEAST_VERSION_STRING);
response.set(http::field::content_type, "application/json");
response.set(http::field::access_control_allow_origin, "*");
response.keep_alive(request.keep_alive());
HTTPHeaders headers;
headers.target = std::string{ request.target() };
for (auto const& val : request.base()) {
headers.httpHeaders[std::string{ val.name_string() }] = std::string{ val.value() };
}
std::cout << "Headers Constructed" << std::endl;
std::cout << "Target: \"" << headers.target << "\"" << std::endl;
if (request.method() == http::verb::get) {
std::cout << "Identified as a GET" << std::endl;
if (auto it = parent->getFunctions.find(headers.target); it != parent->getFunctions.end()) {
std::cout << "Target found" << std::endl;
response.body() = it->second(headers);
response.content_length(response.body().size());
}
else {
std::cout << "Target NOT found" << std::endl;
return do_send(not_found(headers.target));
}
}
else if (request.method() == http::verb::post) {
std::cout << "Identified as a POST" << std::endl;
if (auto it = parent->postFunctions.find(headers.target); it != parent->postFunctions.end()) {
std::cout << "Target found" << std::endl;
response.body() = it->second(headers, request.body());
response.content_length(response.body().size());
}
else {
std::cout << "Target NOT found" << std::endl;
return do_send(not_found(headers.target));
}
}
else {
std::cout << "Bad Request with unknown HTTP verb '" << request.method() << "'" << std::endl;
return do_send(bad_request("An error occurred: 'Unknown HTTP-Method'"));
}
std::cout << "Sending Response" << std::endl;
response.prepare_payload();
return do_send(response);
}
void do_send(string_response response) {
this->response = std::move(response);
http::async_write(
stream,
this->response,
[ptr = shared_from_this()](beast::error_code ec, size_t bytes_transferred) {
ptr->handle_send(ptr->response.need_eof(), ec, bytes_transferred);
}
);
}
void handle_send(bool close, beast::error_code ec, size_t bytes_transferred) {
if (ec) {
std::cerr << "Problem writing to Socket: " << ec.what() << std::endl;
return;
}
if (close) {
return do_close();
}
do_read();
}
public:
RestServerSession(RestServerImpl* parent, tcp::socket socket) :
parent(parent),
stream(std::move(socket)) {
}
};
void stop() {
shouldStop = true;
ioContext.stop();
}
void join() {
for (auto& thread : threads) {
thread.join();
}
}
std::optional<GETFunc> registerGET(std::string_view name, GETFunc func) {
std::string n{ name };
if (auto it = getFunctions.find(n); it != getFunctions.end()) {
std::swap(it->second, func);
return func;
}
else {
getFunctions[n] = std::move(func);
return {};
}
}
std::optional<GETFunc> unregisterGET(std::string_view name) {
std::string n{ name };
if (auto it = getFunctions.find(n); it != getFunctions.end()) {
auto ret = make_optional(std::move(it->second));
getFunctions.erase(it);
return ret;
}
else {
return {};
}
}
std::optional<POSTFunc> registerPOST(std::string_view name, POSTFunc func) {
std::string n{ name };
if (auto it = postFunctions.find(n); it != postFunctions.end()) {
std::swap(it->second, func);
return func;
}
else {
postFunctions[n] = std::move(func);
return {};
}
}
std::optional<POSTFunc> unregisterPOST(std::string_view name) {
std::string n{ name };
if (auto it = postFunctions.find(n); it != postFunctions.end()) {
auto ret = make_optional(std::move(it->second));
postFunctions.erase(it);
return ret;
}
else {
return {};
}
}
void do_accept() {
acceptor.async_accept(
networking::make_strand(ioContext),
[this](beast::error_code ec, tcp::socket socket) {
handle_accept(ec, std::move(socket));
}
);
}
void handle_accept(beast::error_code ec, tcp::socket socket) {
if (ec) {
std::cerr << "Problem Accepting Connection: " << ec.what() << std::endl;
return;
}
auto session = std::make_shared<RestServerSession>(this, std::move(socket));
session->do_read();
do_accept();
}
public:
RestServerImpl(std::string_view name, uint16_t port, uint32_t maxThreadCount):
ioContext(maxThreadCount <= 1'024 ? maxThreadCount : 1'024),
acceptor(networking::make_strand(ioContext)) {
shouldStop = false;
//Does 1024 make sense as an upper limit? IDK lol
maxThreadCount = maxThreadCount <= 1'024 ? maxThreadCount : 1'024;
for (uint32_t i = 0; i < maxThreadCount; i++) {
threads.emplace_back([this] {workFunc(shouldStop, ioContext); });
}
auto address = networking::ip::make_address("0.0.0.0");
tcp::endpoint endpoint{ address, port };
acceptor.open(endpoint.protocol());
acceptor.set_option(networking::socket_base::reuse_address(true));
acceptor.bind(endpoint);
acceptor.listen();
}
};
RestServer::RestServer(std::string_view name, uint16_t port, uint32_t maxThreadCount) {
impl = std::make_unique<RestServerImpl>(name, port, maxThreadCount);
}
RestServer::~RestServer() {
impl->stop();
impl->join();
}
void RestServer::start(std::unordered_map<std::string, GETFunc> getFunctions, std::unordered_map<std::string, POSTFunc> postFunctions) {
for (auto& [name, func] : getFunctions) {
impl->getFunctions[name] = std::move(func);
}
for (auto& [name, func] : postFunctions) {
impl->postFunctions[name] = std::move(func);
}
impl->do_accept();
}
}
Main.cpp
#include<Network/RestServer.h>
#include<boost/json.hpp>
#include<iostream>
#include<thread>
int main() {
net::RestServer server{ "Test Server", 9999 };
server.start(
{
{
"/hello",
[](net::HTTPHeaders const&) {
boost::json::object object;
object["name"] = "Hello World!";
return serialize(object);
}
}
}
, {
{
"/posttest",
[](net::HTTPHeaders const&, std::string const& body) {
auto object = boost::json::parse(body).as_object();
std::string name{ object["name"].as_string()};
object["name"] = name + name;
return serialize(object);
}
}
}
);
std::cout << "Press [ENTER] to stop the server." << std::endl;
std::jthread stopThread{ [] {
std::string line;
std::getline(std::cin, line);
} };
}
game-data.service.ts (typescript)
public getHelloWorld():Observable<{name:string}> {
return this.httpClient.get<{name:string}>("http://localhost:9999/hello");
}
public getPostTest(name:string):Observable<{name:string}> {
return this.httpClient.post<{name:string}>("http://localhost:9999/posttest", {name:name});
}
Output from the Server Console
Press [ENTER] to stop the server.
Handling RequestHandling Request
Headers Constructed
Target: "/posttest"
Bad Request with unknown HTTP verb 'OPTIONS'
Headers Constructed
Target: "/hello"
Identified as a GET
Target found
Sending Response
Problem reading from Socket: Problem reading from Socket: The socket was closed due to a timeout [boost.beast:1]The socket was closed due to a timeout [boost.beast:1]
Problem reading from Socket: The socket was closed due to a timeout [boost.beast:1]
Output from Server Console when post is disabled in typescript
Press [ENTER] to stop the server.
Handling Request
Headers Constructed
Target: "/hello"
Identified as a GET
Target found
Sending Response