diff --git a/lib/src/HttpRequestImpl.cc b/lib/src/HttpRequestImpl.cc index a11e4ea299..1562fb977a 100644 --- a/lib/src/HttpRequestImpl.cc +++ b/lib/src/HttpRequestImpl.cc @@ -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(), - content.length() + content_.length()); + content.length() + content_.length() + cachedBodyLength); output->append(buf, len); if (contentTypeString_.empty()) { @@ -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, diff --git a/lib/tests/CMakeLists.txt b/lib/tests/CMakeLists.txt index c8249b41ab..1eb0c6d706 100644 --- a/lib/tests/CMakeLists.txt +++ b/lib/tests/CMakeLists.txt @@ -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 @@ -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 diff --git a/lib/tests/unittests/HttpRequestBodyCacheTest.cc b/lib/tests/unittests/HttpRequestBodyCacheTest.cc new file mode 100644 index 0000000000..4bcd635701 --- /dev/null +++ b/lib/tests/unittests/HttpRequestBodyCacheTest.cc @@ -0,0 +1,79 @@ +#define DROGON_TEST_MAIN +#include +#include +#include +#include +#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); +} diff --git a/lib/tests/unittests/test_shim_windows_shared.cc b/lib/tests/unittests/test_shim_windows_shared.cc new file mode 100644 index 0000000000..595553ef1a --- /dev/null +++ b/lib/tests/unittests/test_shim_windows_shared.cc @@ -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