Skip to content
12 changes: 10 additions & 2 deletions lib/src/HttpRequestImpl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -371,14 +371,16 @@ void HttpRequestImpl::appendToBuffer(trantor::MsgBuffer *output) const
assert(!(!content.empty() && !content_.empty()));
if (!passThrough_)
{
if (!content.empty() || !content_.empty())
const auto cachedBodyLength =
cacheFilePtr_ ? cacheFilePtr_->getStringView().length() : 0;
if (!content.empty() || !content_.empty() || cachedBodyLength != 0)
{
char buf[64];
auto len = snprintf(
buf,
sizeof(buf),
contentLengthFormatString<decltype(content.length())>(),
content.length() + content_.length());
content.length() + content_.length() + cachedBodyLength);
output->append(buf, len);
if (contentTypeString_.empty())
{
Expand Down Expand Up @@ -426,6 +428,12 @@ void HttpRequestImpl::appendToBuffer(trantor::MsgBuffer *output) const
output->append(content);
if (!content_.empty())
output->append(content_);
if (cacheFilePtr_)
{
auto bodyPieceView = cacheFilePtr_->getStringView();
if (!bodyPieceView.empty())
output->append(bodyPieceView.data(), bodyPieceView.length());
}
}

void HttpRequestImpl::addHeader(const char *start,
Expand Down
9 changes: 8 additions & 1 deletion lib/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ set(UNITTEST_SOURCES
unittests/CacheMapTest.cc
unittests/StringOpsTest.cc
unittests/ControllerCreationTest.cc
unittests/HttpRequestBodyCacheTest.cc
unittests/MultiPartParserTest.cc
unittests/SlashRemoverTest.cc
unittests/UtilitiesTest.cc
Expand All @@ -39,7 +40,13 @@ if(Brotli_FOUND)
endif()

if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC" AND BUILD_SHARED_LIBS)
set(UNITTEST_SOURCES ${UNITTEST_SOURCES} ../src/HttpUtils.cc)
# Follow the existing pattern used by other tests: only compile
# HttpUtils.cc into the unittest target for MSVC shared builds.
# Use a minimal test shim to provide symbols needed by tests (like
# HttpRequestBodyCacheTest) without pulling in platform-specific
# dependencies or unresolved external symbols from the shared library.
set(UNITTEST_SOURCES ${UNITTEST_SOURCES} ../src/HttpUtils.cc
unittests/test_shim_windows_shared.cc)
else()
set(UNITTEST_SOURCES ${UNITTEST_SOURCES} ../src/HttpFileImpl.cc
unittests/HttpFileTest.cc
Expand Down
79 changes: 79 additions & 0 deletions lib/tests/unittests/HttpRequestBodyCacheTest.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#define DROGON_TEST_MAIN
#include <drogon/drogon_test.h>
#include <filesystem>
#include <trantor/net/EventLoop.h>
#include <trantor/utils/MsgBuffer.h>
#include "../../lib/src/HttpAppFrameworkImpl.h"
#include "../../lib/src/HttpRequestImpl.h"

using namespace drogon;
using namespace trantor;

namespace
{
struct BodyLimitGuard
{
HttpAppFrameworkImpl &app;
size_t oldLimit;

~BodyLimitGuard()
{
app.setClientMaxMemoryBodySize(oldLimit);
}
};

struct UploadPathGuard
{
HttpAppFrameworkImpl &app;
std::string oldPath;

~UploadPathGuard()
{
app.setUploadPath(oldPath);
}
};
} // namespace

DROGON_TEST(CachedRequestBodyIsSerialized)
{
auto &appFramework = HttpAppFrameworkImpl::instance();
BodyLimitGuard guard{appFramework,
appFramework.getClientMaxMemoryBodySize()};
auto tempRoot = std::filesystem::temp_directory_path() /
"drogon-request-body-cache-test";
std::filesystem::remove_all(tempRoot);
std::filesystem::create_directories(tempRoot);
for (char hi : std::string{"0123456789abcdefABCDEF"})
{
for (char lo : std::string{"0123456789abcdefABCDEF"})
{
std::filesystem::create_directories(tempRoot / "tmp" /
std::string{hi, lo});
}
}
UploadPathGuard uploadGuard{appFramework, appFramework.getUploadPath()};
appFramework.setUploadPath(tempRoot.string());
appFramework.setClientMaxMemoryBodySize(1);

EventLoop loop;
HttpRequestImpl req(&loop);
req.setMethod(Post);
req.setPath("/forward");
req.setVersion(Version::kHttp11);

const std::string body(32, 'x');
req.reserveBodySize(body.size());
req.appendToBody(body.data(), body.size());

MsgBuffer buffer;
req.appendToBuffer(&buffer);

std::string serialized(buffer.peek(), buffer.readableBytes());
CHECK(serialized.find("content-length: 32\r\n") != std::string::npos);

auto separatorPos = serialized.find("\r\n\r\n");
REQUIRE(separatorPos != std::string::npos);
CHECK(serialized.substr(separatorPos + 4) == body);

std::filesystem::remove_all(tempRoot);
}
99 changes: 99 additions & 0 deletions lib/tests/unittests/test_shim_windows_shared.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Minimal test shim for MSVC shared builds to provide small, portable
// implementations used by `HttpRequestBodyCacheTest` without pulling in
// platform-specific sources like CacheFile.cc.

#if defined(_MSC_VER) && defined(BUILD_SHARED_LIBS)

#include "../../lib/src/HttpAppFrameworkImpl.h"
#include "../../lib/src/HttpRequestImpl.h"

namespace drogon
{

// Provide a lightweight setUploadPath implementation so tests can set
// upload path without linking to the full HttpAppFrameworkImpl object
// implementation in the DLL (which may not export this symbol on MSVC).
HttpAppFramework &HttpAppFrameworkImpl::setUploadPath(
const std::string &uploadPath)
{
uploadPath_ = uploadPath;
return *this;
}

// Simplified, in-memory implementations for body handling used by the test.
void HttpRequestImpl::reserveBodySize(size_t /*length*/)
{
// For tests, allow storing body in-memory to avoid file-backed cache.
}

void HttpRequestImpl::appendToBody(const char *data, size_t length)
{
realContentLength_ += length;
// Store everything in memory for the shim
content_.append(data, length);
}

void HttpRequestImpl::appendToBuffer(trantor::MsgBuffer *output) const
{
// Minimal serialization: METHOD SP PATH [?query] SP HTTP/1.1\r\n
switch (method_)
{
case Get:
output->append("GET ");
break;
case Post:
output->append("POST ");
break;
case Head:
output->append("HEAD ");
break;
case Put:
output->append("PUT ");
break;
case Delete:
output->append("DELETE ");
break;
default:
output->append("GET ");
break;
}

if (!path_.empty())
{
output->append(path_);
}
else
{
output->append("/");
}

if (!query_.empty())
{
output->append("?");
output->append(query_);
}

output->append(" HTTP/1.1\r\n");

// content-length header
if (!content_.empty())
{
char buf[64];
auto len = snprintf(buf, sizeof(buf), "content-length: %zu\r\n",
content_.length());
output->append(buf, len);
}
else if (method_ == Post || method_ == Put)
{
output->append("content-length: 0\r\n");
}

// Append a blank line then the body
output->append("\r\n");
if (!content_.empty())
output->append(content_);
}

} // namespace drogon

#endif
Loading