From 3c7ab8c5128645c45b8573d21c7c064b35786863 Mon Sep 17 00:00:00 2001 From: Matheus Gabriel Werny Date: Tue, 23 Sep 2025 17:50:38 +0200 Subject: [PATCH 1/3] ETag for static file server Programmed feature https://github.com/yhirose/cpp-httplib/issues/2242. First of, the static file server now sends HTTP response header "ETag". Following HTTP requests by the client which include HTTP request header "If-None-Match" are only served if the value for HTTP response header "ETag" is not included in the value of HTTP request header "If-None-Match", otherwise an HTTP response with status code 304 is served which includes the HTTP response header "ETag" again that would have been sent with a normal status code of 200. Useful resources: - https://http.dev/caching - https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag - https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/If-None-Match - https://www.rfc-editor.org/rfc/rfc9110.html#name-304-not-modified --- httplib.h | 38 ++++++++++++++++++++++++++++++++++++++ test/test.cc | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/httplib.h b/httplib.h index db55d07e25..80e316b410 100644 --- a/httplib.h +++ b/httplib.h @@ -7881,6 +7881,44 @@ inline bool Server::handle_file_request(const Request &req, Response &res) { return false; } +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + // Value for HTTP response header ETag. + const std::string etag = + R"(")" + detail::SHA_512(mm->data()) + R"(")"; + res.set_header("ETag", etag); + + if (req.has_header("If-None-Match")) { + const std::string header_if_none_match = + req.get_header_value("If-None-Match"); + + /* + * Values of HTTP request header If-None-Match which are cached + * values of previous HTTP response header ETag. + */ + std::set etags; + detail::split(header_if_none_match.c_str(), + header_if_none_match.c_str() + + header_if_none_match.length(), + ',', [&](const char *b, const char *e) { + std::string etag(b, e); + + // Weak validation is not supported. + if (etag.length() >= 2 && etag.at(0) == 'W' && + etag.at(1) == '/') { + etag.erase(0, 2); + } + + etags.insert(std::move(etag)); + }); + + if (etags.find("*") != etags.cend() || + etags.find(etag) != etags.cend()) { + res.status = StatusCode::NotModified_304; + return true; + } + } +#endif + res.set_content_provider( mm->size(), detail::find_content_type(path, file_extension_and_mimetype_map_, diff --git a/test/test.cc b/test/test.cc index a9ac0d17f1..97df13bbd3 100644 --- a/test/test.cc +++ b/test/test.cc @@ -10828,3 +10828,43 @@ TEST(HeaderSmugglingTest, ChunkedTrailerHeadersMerged) { std::string res; ASSERT_TRUE(send_request(1, req, &res)); } + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +TEST(StaticFileSever, CacheValidation) { + for ( + const std::string header_if_none_match : { + R"("db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b")", + R"("d", "db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b")", + R"("db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b", "g")", + R"(*)", + R"("f", *)", + R"(*, "i")", + }) { + httplib::Server svr; + svr.set_mount_point("/", "./www/"); + + std::thread t = thread([&]() { svr.listen(HOST, PORT); }); + auto se = detail::scope_exit([&] { + svr.stop(); + t.join(); + ASSERT_FALSE(svr.is_running()); + }); + + svr.wait_until_ready(); + + httplib::Client client(HOST, PORT); + const httplib::Result result = + client.Get("/file", Headers({{"If-None-Match", header_if_none_match}})); + + ASSERT_NE(result, nullptr); + EXPECT_EQ(result.error(), Error::Success); + EXPECT_EQ(result->status, StatusCode::NotModified_304); + + EXPECT_TRUE(result->has_header("ETag")); + EXPECT_EQ( + result->get_header_value("ETag"), + R"("db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b")"); + EXPECT_TRUE(result->body.empty()); + } +} +#endif From fd88931bc0e5dbcce8af54ab300180476bc4d523 Mon Sep 17 00:00:00 2001 From: Matheus Gabriel Werny Date: Wed, 24 Sep 2025 13:19:21 +0200 Subject: [PATCH 2/3] Minor adjustments Added more comments. Check for method to be GET or HEAD. Explicitly clear the body. --- httplib.h | 80 ++++++++++++++++++++++++++++++++-------------------- test/test.cc | 13 ++++----- 2 files changed, 55 insertions(+), 38 deletions(-) diff --git a/httplib.h b/httplib.h index 80e316b410..5cc2abf3bf 100644 --- a/httplib.h +++ b/httplib.h @@ -7882,39 +7882,59 @@ inline bool Server::handle_file_request(const Request &req, Response &res) { } #ifdef CPPHTTPLIB_OPENSSL_SUPPORT - // Value for HTTP response header ETag. - const std::string etag = - R"(")" + detail::SHA_512(mm->data()) + R"(")"; - res.set_header("ETag", etag); - - if (req.has_header("If-None-Match")) { - const std::string header_if_none_match = - req.get_header_value("If-None-Match"); + /* + * The HTTP request header If-None-Match can be used with other + * methods where it has the meaning to only execute if the resource + * does not already exist but uploading does not matter here. + * HTTP response header ETag is only set where content is + * pulled and not pushed as those HTTP response bodies do not have to + * be related to the content. + */ + if (req.method == "GET" || req.method == "HEAD") { + // Value for HTTP response header ETag. + const std::string etag = + R"(")" + detail::SHA_512(mm->data()) + R"(")"; + /* + * Weak validation is not used. + * HTTP response header ETag must be set as if normal HTTP response + * was sent. + */ + res.set_header("ETag", etag); /* - * Values of HTTP request header If-None-Match which are cached - * values of previous HTTP response header ETag. + * Semantic: If value exists, the server will send status code 304. + * * always results in status code 304. */ - std::set etags; - detail::split(header_if_none_match.c_str(), - header_if_none_match.c_str() + - header_if_none_match.length(), - ',', [&](const char *b, const char *e) { - std::string etag(b, e); - - // Weak validation is not supported. - if (etag.length() >= 2 && etag.at(0) == 'W' && - etag.at(1) == '/') { - etag.erase(0, 2); - } - - etags.insert(std::move(etag)); - }); - - if (etags.find("*") != etags.cend() || - etags.find(etag) != etags.cend()) { - res.status = StatusCode::NotModified_304; - return true; + if (req.has_header("If-None-Match")) { + const std::string header_if_none_match = + req.get_header_value("If-None-Match"); + + /* + * Values of HTTP request header If-None-Match which are cached + * values of previous HTTP response header ETag. + */ + std::set etags; + detail::split(header_if_none_match.c_str(), + header_if_none_match.c_str() + + header_if_none_match.length(), + ',', [&](const char *b, const char *e) { + std::string etag(b, e); + + // Weak validation is not used. + if (etag.length() >= 2 && etag.at(0) == 'W' && + etag.at(1) == '/') { + etag.erase(0, 2); + } + + etags.insert(std::move(etag)); + }); + + if (etags.find("*") != etags.cend() || + etags.find(etag) != etags.cend()) { + res.status = StatusCode::NotModified_304; + res.body.clear(); + return true; + } } } #endif diff --git a/test/test.cc b/test/test.cc index 97df13bbd3..d6df76b863 100644 --- a/test/test.cc +++ b/test/test.cc @@ -10832,14 +10832,11 @@ TEST(HeaderSmugglingTest, ChunkedTrailerHeadersMerged) { #ifdef CPPHTTPLIB_OPENSSL_SUPPORT TEST(StaticFileSever, CacheValidation) { for ( - const std::string header_if_none_match : { - R"("db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b")", - R"("d", "db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b")", - R"("db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b", "g")", - R"(*)", - R"("f", *)", - R"(*, "i")", - }) { + const std::string header_if_none_match : + {"*", R"("f", *)", R"(*, "i")", + R"("db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b")", + R"("d", "db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b")", + R"(W/"db88b784d27f0b92b63f0b3b159ea6f049b178546d99ae95f6f7b57c678c61c2d4b50af4374e81a09e812c2c957a5353803cef4c34aa36fe937ae643cc86bb4b", "g")"}) { httplib::Server svr; svr.set_mount_point("/", "./www/"); From d38da7b7aff3372cdc85e423e903a2139416b371 Mon Sep 17 00:00:00 2001 From: Matheus Gabriel Werny Date: Sat, 27 Sep 2025 21:42:47 +0200 Subject: [PATCH 3/3] Bug fix Fixed c-string without null termination. --- httplib.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/httplib.h b/httplib.h index 5cc2abf3bf..377b59b68d 100644 --- a/httplib.h +++ b/httplib.h @@ -7892,8 +7892,9 @@ inline bool Server::handle_file_request(const Request &req, Response &res) { */ if (req.method == "GET" || req.method == "HEAD") { // Value for HTTP response header ETag. + const std::string file_data(mm->data(), mm->size()); const std::string etag = - R"(")" + detail::SHA_512(mm->data()) + R"(")"; + R"(")" + detail::SHA_512(file_data) + R"(")"; /* * Weak validation is not used. * HTTP response header ETag must be set as if normal HTTP response