Parcourir la source

Implement ETag and Last-Modified support for static file responses and If-Range requests (#2286)

* Fix #2242: Implement ETag and Last-Modified support for static file responses

* Add ETag and Last-Modified handling for If-Range requests

* Enhance HTTP date parsing with improved error handling and locale support

* Update httplib.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update test/test.cc

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update httplib.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Refactor ETag handling: separate strong and weak ETag checks for If-Range requests

* Fix type for mtime in FileStat and improve ETag handling comments

* Update httplib.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Resolved code review comments

* Update httplib.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update httplib.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Refactor ETag handling: use 'auto' for type inference and improve code readability

* Refactor ETag handling: extract check_if_not_modified and check_if_range methods for improved readability and maintainability

* Code cleanup

* Update httplib.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update test/test.cc

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update httplib.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update httplib.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Enhance ETag handling and validation in httplib.h and add comprehensive tests in test.cc

* Refactor ETag comparison logic and add test for If-None-Match with non-existent file

* Fix #2287

* Code cleanup

* Add tests for extreme date values and negative file modification time in ETag handling

* Update HTTP-date parsing comments to reference RFC 9110

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
yhirose il y a 2 mois
Parent
commit
3e0fa33559
2 fichiers modifiés avec 768 ajouts et 16 suppressions
  1. 233 16
      httplib.h
  2. 535 0
      test/test.cc

+ 233 - 16
httplib.h

@@ -1011,17 +1011,6 @@ using ErrorLogger = std::function<void(const Error &, const Request *)>;
 
 using SocketOptions = std::function<void(socket_t sock)>;
 
-namespace detail {
-
-bool set_socket_opt_impl(socket_t sock, int level, int optname,
-                         const void *optval, socklen_t optlen);
-bool set_socket_opt(socket_t sock, int level, int optname, int opt);
-bool set_socket_opt_time(socket_t sock, int level, int optname, time_t sec,
-                         time_t usec);
-int close_socket(socket_t sock);
-
-} // namespace detail
-
 void default_socket_options(socket_t sock);
 
 const char *status_message(int status);
@@ -1102,10 +1091,9 @@ private:
   std::regex regex_;
 };
 
-ssize_t write_headers(Stream &strm, const Headers &headers);
+int close_socket(socket_t sock);
 
-std::string make_host_and_port_string(const std::string &host, int port,
-                                      bool is_ssl);
+ssize_t write_headers(Stream &strm, const Headers &headers);
 
 } // namespace detail
 
@@ -1257,7 +1245,11 @@ private:
   bool listen_internal();
 
   bool routing(Request &req, Response &res, Stream &strm);
-  bool handle_file_request(const Request &req, Response &res);
+  bool handle_file_request(Request &req, Response &res);
+  bool check_if_not_modified(const Request &req, Response &res,
+                             const std::string &etag, time_t mtime) const;
+  bool check_if_range(Request &req, const std::string &etag,
+                      time_t mtime) const;
   bool dispatch_request(Request &req, Response &res,
                         const Handlers &handlers) const;
   bool dispatch_request_for_content_reader(
@@ -2593,6 +2585,8 @@ struct FileStat {
   FileStat(const std::string &path);
   bool is_file() const;
   bool is_dir() const;
+  time_t mtime() const;
+  size_t size() const;
 
 private:
 #if defined(_WIN32)
@@ -2603,6 +2597,9 @@ private:
   int ret_ = -1;
 };
 
+std::string make_host_and_port_string(const std::string &host, int port,
+                                      bool is_ssl);
+
 std::string trim_copy(const std::string &s);
 
 void divide(
@@ -2971,6 +2968,90 @@ inline std::string from_i_to_hex(size_t n) {
   return ret;
 }
 
+inline std::string compute_etag(const FileStat &fs) {
+  if (!fs.is_file()) { return std::string(); }
+
+  // If mtime cannot be determined (negative value indicates an error
+  // or sentinel), do not generate an ETag. Returning a neutral / fixed
+  // value like 0 could collide with a real file that legitimately has
+  // mtime == 0 (epoch) and lead to misleading validators.
+  auto mtime_raw = fs.mtime();
+  if (mtime_raw < 0) { return std::string(); }
+
+  auto mtime = static_cast<size_t>(mtime_raw);
+  auto size = fs.size();
+
+  return std::string("W/\"") + from_i_to_hex(mtime) + "-" +
+         from_i_to_hex(size) + "\"";
+}
+
+// Format time_t as HTTP-date (RFC 9110 Section 5.6.7): "Sun, 06 Nov 1994
+// 08:49:37 GMT" This implementation is defensive: it validates `mtime`, checks
+// return values from `gmtime_r`/`gmtime_s`, and ensures `strftime` succeeds.
+inline std::string file_mtime_to_http_date(time_t mtime) {
+  if (mtime < 0) { return std::string(); }
+
+  struct tm tm_buf;
+#ifdef _WIN32
+  if (gmtime_s(&tm_buf, &mtime) != 0) { return std::string(); }
+#else
+  if (gmtime_r(&mtime, &tm_buf) == nullptr) { return std::string(); }
+#endif
+  char buf[64];
+  if (strftime(buf, sizeof(buf), "%a, %d %b %Y %H:%M:%S GMT", &tm_buf) == 0) {
+    return std::string();
+  }
+
+  return std::string(buf);
+}
+
+// Parse HTTP-date (RFC 9110 Section 5.6.7) to time_t. Returns -1 on failure.
+inline time_t parse_http_date(const std::string &date_str) {
+  struct tm tm_buf;
+
+  // Create a classic locale object once for all parsing attempts
+  const std::locale classic_locale = std::locale::classic();
+
+  // Try to parse using std::get_time (C++11, cross-platform)
+  auto try_parse = [&](const char *fmt) -> bool {
+    std::istringstream ss(date_str);
+    ss.imbue(classic_locale);
+
+    memset(&tm_buf, 0, sizeof(tm_buf));
+    ss >> std::get_time(&tm_buf, fmt);
+
+    return !ss.fail();
+  };
+
+  // RFC 9110 preferred format (HTTP-date): "Sun, 06 Nov 1994 08:49:37 GMT"
+  if (!try_parse("%a, %d %b %Y %H:%M:%S")) {
+    // RFC 850 format: "Sunday, 06-Nov-94 08:49:37 GMT"
+    if (!try_parse("%A, %d-%b-%y %H:%M:%S")) {
+      // asctime format: "Sun Nov  6 08:49:37 1994"
+      if (!try_parse("%a %b %d %H:%M:%S %Y")) {
+        return static_cast<time_t>(-1);
+      }
+    }
+  }
+
+#ifdef _WIN32
+  return _mkgmtime(&tm_buf);
+#else
+  return timegm(&tm_buf);
+#endif
+}
+
+inline bool is_weak_etag(const std::string &s) {
+  // Check if the string is a weak ETag (starts with 'W/"')
+  return s.size() > 3 && s[0] == 'W' && s[1] == '/' && s[2] == '"';
+}
+
+inline bool is_strong_etag(const std::string &s) {
+  // Check if the string is a strong ETag (starts and ends with '"', at least 2
+  // chars)
+  return s.size() >= 2 && s[0] == '"' && s.back() == '"';
+}
+
 inline size_t to_utf8(int code, char *buff) {
   if (code < 0x0080) {
     buff[0] = static_cast<char>(code & 0x7F);
@@ -3090,6 +3171,15 @@ inline bool FileStat::is_dir() const {
   return ret_ >= 0 && S_ISDIR(st_.st_mode);
 }
 
+inline time_t FileStat::mtime() const {
+  return ret_ >= 0 ? static_cast<time_t>(st_.st_mtime)
+                   : static_cast<time_t>(-1);
+}
+
+inline size_t FileStat::size() const {
+  return ret_ >= 0 ? static_cast<size_t>(st_.st_size) : 0;
+}
+
 inline std::string encode_path(const std::string &s) {
   std::string result;
   result.reserve(s.size());
@@ -3345,6 +3435,42 @@ inline void split(const char *b, const char *e, char d, size_t m,
   }
 }
 
+inline bool split_find(const char *b, const char *e, char d, size_t m,
+                       std::function<bool(const char *, const char *)> fn) {
+  size_t i = 0;
+  size_t beg = 0;
+  size_t count = 1;
+
+  while (e ? (b + i < e) : (b[i] != '\0')) {
+    if (b[i] == d && count < m) {
+      auto r = trim(b, e, beg, i);
+      if (r.first < r.second) {
+        auto found = fn(&b[r.first], &b[r.second]);
+        if (found) { return true; }
+      }
+      beg = i + 1;
+      count++;
+    }
+    i++;
+  }
+
+  if (i) {
+    auto r = trim(b, e, beg, i);
+    if (r.first < r.second) {
+      auto found = fn(&b[r.first], &b[r.second]);
+      if (found) { return true; }
+    }
+  }
+
+  return false;
+}
+
+inline bool split_find(const char *b, const char *e, char d,
+                       std::function<bool(const char *, const char *)> fn) {
+  return split_find(b, e, d, (std::numeric_limits<size_t>::max)(),
+                    std::move(fn));
+}
+
 inline stream_line_reader::stream_line_reader(Stream &strm, char *fixed_buffer,
                                               size_t fixed_buffer_size)
     : strm_(strm), fixed_buffer_(fixed_buffer),
@@ -8256,7 +8382,7 @@ inline bool Server::read_content_core(
   return true;
 }
 
-inline bool Server::handle_file_request(const Request &req, Response &res) {
+inline bool Server::handle_file_request(Request &req, Response &res) {
   for (const auto &entry : base_dirs_) {
     // Prefix match
     if (!req.path.compare(0, entry.mount_point.size(), entry.mount_point)) {
@@ -8277,6 +8403,20 @@ inline bool Server::handle_file_request(const Request &req, Response &res) {
             res.set_header(kv.first, kv.second);
           }
 
+          auto etag = detail::compute_etag(stat);
+          if (!etag.empty()) { res.set_header("ETag", etag); }
+
+          auto mtime = stat.mtime();
+
+          auto last_modified = detail::file_mtime_to_http_date(mtime);
+          if (!last_modified.empty()) {
+            res.set_header("Last-Modified", last_modified);
+          }
+
+          if (check_if_not_modified(req, res, etag, mtime)) { return true; }
+
+          check_if_range(req, etag, mtime);
+
           auto mm = std::make_shared<detail::mmap>(path.c_str());
           if (!mm->is_open()) {
             output_error_log(Error::OpenFile, &req);
@@ -8306,6 +8446,79 @@ inline bool Server::handle_file_request(const Request &req, Response &res) {
   return false;
 }
 
+inline bool Server::check_if_not_modified(const Request &req, Response &res,
+                                          const std::string &etag,
+                                          time_t mtime) const {
+  // Handle conditional GET:
+  // 1. If-None-Match takes precedence (RFC 9110 Section 13.1.2)
+  // 2. If-Modified-Since is checked only when If-None-Match is absent
+  if (req.has_header("If-None-Match")) {
+    if (!etag.empty()) {
+      auto val = req.get_header_value("If-None-Match");
+
+      // NOTE: We use exact string matching here. This works correctly
+      // because our server always generates weak ETags (W/"..."), and
+      // clients typically send back the same ETag they received.
+      // RFC 9110 Section 8.8.3.2 allows weak comparison for
+      // If-None-Match, where W/"x" and "x" would match, but this
+      // simplified implementation requires exact matches.
+      auto ret = detail::split_find(val.data(), val.data() + val.size(), ',',
+                                    [&](const char *b, const char *e) {
+                                      return std::equal(b, e, "*") ||
+                                             std::equal(b, e, etag.begin());
+                                    });
+
+      if (ret) {
+        res.status = StatusCode::NotModified_304;
+        return true;
+      }
+    }
+  } else if (req.has_header("If-Modified-Since")) {
+    auto val = req.get_header_value("If-Modified-Since");
+    auto t = detail::parse_http_date(val);
+
+    if (t != static_cast<time_t>(-1) && mtime <= t) {
+      res.status = StatusCode::NotModified_304;
+      return true;
+    }
+  }
+  return false;
+}
+
+inline bool Server::check_if_range(Request &req, const std::string &etag,
+                                   time_t mtime) const {
+  // Handle If-Range for partial content requests (RFC 9110
+  // Section 13.1.5). If-Range is only evaluated when Range header is
+  // present. If the validator matches, serve partial content; otherwise
+  // serve full content.
+  if (!req.ranges.empty() && req.has_header("If-Range")) {
+    auto val = req.get_header_value("If-Range");
+
+    auto is_valid_range = [&]() {
+      if (detail::is_strong_etag(val)) {
+        // RFC 9110 Section 13.1.5: If-Range requires strong ETag
+        // comparison.
+        return (!etag.empty() && val == etag);
+      } else if (detail::is_weak_etag(val)) {
+        // Weak ETags are not valid for If-Range (RFC 9110 Section 13.1.5)
+        return false;
+      } else {
+        // HTTP-date comparison
+        auto t = detail::parse_http_date(val);
+        return (t != static_cast<time_t>(-1) && mtime <= t);
+      }
+    };
+
+    if (!is_valid_range()) {
+      // Validator doesn't match: ignore Range and serve full content
+      req.ranges.clear();
+      return false;
+    }
+  }
+
+  return true;
+}
+
 inline socket_t
 Server::create_server_socket(const std::string &host, int port,
                              int socket_flags,
@@ -8573,10 +8786,13 @@ inline void Server::apply_ranges(const Request &req, Response &res,
           res.set_header("Transfer-Encoding", "chunked");
           if (type == detail::EncodingType::Gzip) {
             res.set_header("Content-Encoding", "gzip");
+            res.set_header("Vary", "Accept-Encoding");
           } else if (type == detail::EncodingType::Brotli) {
             res.set_header("Content-Encoding", "br");
+            res.set_header("Vary", "Accept-Encoding");
           } else if (type == detail::EncodingType::Zstd) {
             res.set_header("Content-Encoding", "zstd");
+            res.set_header("Vary", "Accept-Encoding");
           }
         }
       }
@@ -8635,6 +8851,7 @@ inline void Server::apply_ranges(const Request &req, Response &res,
                                  })) {
           res.body.swap(compressed);
           res.set_header("Content-Encoding", content_encoding);
+          res.set_header("Vary", "Accept-Encoding");
         }
       }
     }

+ 535 - 0
test/test.cc

@@ -4,9 +4,11 @@
 
 #ifndef _WIN32
 #include <arpa/inet.h>
+#include <ctime>
 #include <curl/curl.h>
 #include <netinet/in.h>
 #include <sys/socket.h>
+#include <sys/time.h>
 #include <unistd.h>
 #endif
 #include <gtest/gtest.h>
@@ -12687,3 +12689,536 @@ TEST(ErrorHandlingTest, SSLStreamConnectionClosed) {
   t.join();
 }
 #endif
+
+TEST(ETagTest, StaticFileETagAndIfNoneMatch) {
+  using namespace httplib;
+
+  // Create a test file
+  const char *fname = "etag_testfile.txt";
+  const char *content = "etag-content";
+  {
+    std::ofstream ofs(fname);
+    ofs << content;
+    ASSERT_TRUE(ofs.good());
+  }
+
+  Server svr;
+  svr.set_mount_point("/static", ".");
+  auto t = std::thread([&]() { svr.listen("localhost", 8087); });
+  svr.wait_until_ready();
+
+  Client cli("localhost", 8087);
+
+  // First request: should get 200 with ETag header
+  auto res1 = cli.Get("/static/etag_testfile.txt");
+  ASSERT_TRUE(res1);
+  ASSERT_EQ(200, res1->status);
+  ASSERT_TRUE(res1->has_header("ETag"));
+  std::string etag = res1->get_header_value("ETag");
+  EXPECT_FALSE(etag.empty());
+
+  // Verify ETag format: W/"hex-hex"
+  ASSERT_GE(etag.length(), 5u); // Minimum: W/""
+  EXPECT_EQ('W', etag[0]);
+  EXPECT_EQ('/', etag[1]);
+  EXPECT_EQ('"', etag[2]);
+  EXPECT_EQ('"', etag.back());
+
+  // Exact match: expect 304 Not Modified
+  Headers h2 = {{"If-None-Match", etag}};
+  auto res2 = cli.Get("/static/etag_testfile.txt", h2);
+  ASSERT_TRUE(res2);
+  EXPECT_EQ(304, res2->status);
+
+  // Wildcard match: expect 304 Not Modified
+  Headers h3 = {{"If-None-Match", "*"}};
+  auto res3 = cli.Get("/static/etag_testfile.txt", h3);
+  ASSERT_TRUE(res3);
+  EXPECT_EQ(304, res3->status);
+
+  // Non-matching ETag: expect 200
+  Headers h4 = {{"If-None-Match", "W/\"deadbeef\""}};
+  auto res4 = cli.Get("/static/etag_testfile.txt", h4);
+  ASSERT_TRUE(res4);
+  EXPECT_EQ(200, res4->status);
+
+  // Multiple ETags with one matching: expect 304
+  Headers h5 = {{"If-None-Match", "W/\"other\", " + etag + ", W/\"another\""}};
+  auto res5 = cli.Get("/static/etag_testfile.txt", h5);
+  ASSERT_TRUE(res5);
+  EXPECT_EQ(304, res5->status);
+
+  svr.stop();
+  t.join();
+  std::remove(fname);
+}
+
+TEST(ETagTest, StaticFileETagIfNoneMatchStarNotFound) {
+  using namespace httplib;
+
+  Server svr;
+  svr.set_mount_point("/static", ".");
+  auto t = std::thread([&]() { svr.listen("localhost", 8090); });
+  svr.wait_until_ready();
+
+  Client cli("localhost", 8090);
+
+  // Send If-None-Match: * to a non-existent file
+  Headers h = {{"If-None-Match", "*"}};
+  auto res = cli.Get("/static/etag_testfile_notfound.txt", h);
+  ASSERT_TRUE(res);
+  EXPECT_EQ(404, res->status);
+
+  svr.stop();
+  t.join();
+}
+
+TEST(ETagTest, LastModifiedAndIfModifiedSince) {
+  using namespace httplib;
+
+  // Create a test file
+  const char *fname = "ims_testfile.txt";
+  const char *content = "if-modified-since-test";
+  {
+    std::ofstream ofs(fname);
+    ofs << content;
+    ASSERT_TRUE(ofs.good());
+  }
+
+  Server svr;
+  svr.set_mount_point("/static", ".");
+  auto t = std::thread([&]() { svr.listen("localhost", 8088); });
+  svr.wait_until_ready();
+
+  Client cli("localhost", 8088);
+
+  // First request: should get 200 with Last-Modified header
+  auto res1 = cli.Get("/static/ims_testfile.txt");
+  ASSERT_TRUE(res1);
+  ASSERT_EQ(200, res1->status);
+  ASSERT_TRUE(res1->has_header("Last-Modified"));
+  std::string last_modified = res1->get_header_value("Last-Modified");
+  EXPECT_FALSE(last_modified.empty());
+
+  // If-Modified-Since with same time: expect 304
+  Headers h2 = {{"If-Modified-Since", last_modified}};
+  auto res2 = cli.Get("/static/ims_testfile.txt", h2);
+  ASSERT_TRUE(res2);
+  EXPECT_EQ(304, res2->status);
+
+  // If-Modified-Since with future time: expect 304
+  Headers h3 = {{"If-Modified-Since", "Sun, 01 Jan 2099 00:00:00 GMT"}};
+  auto res3 = cli.Get("/static/ims_testfile.txt", h3);
+  ASSERT_TRUE(res3);
+  EXPECT_EQ(304, res3->status);
+
+  // If-Modified-Since with past time: expect 200
+  Headers h4 = {{"If-Modified-Since", "Sun, 01 Jan 2000 00:00:00 GMT"}};
+  auto res4 = cli.Get("/static/ims_testfile.txt", h4);
+  ASSERT_TRUE(res4);
+  EXPECT_EQ(200, res4->status);
+
+  // If-None-Match takes precedence over If-Modified-Since
+  // (send matching ETag with old If-Modified-Since -> should still be 304)
+  ASSERT_TRUE(res1->has_header("ETag"));
+  std::string etag = res1->get_header_value("ETag");
+  Headers h5 = {{"If-None-Match", etag},
+                {"If-Modified-Since", "Sun, 01 Jan 2000 00:00:00 GMT"}};
+  auto res5 = cli.Get("/static/ims_testfile.txt", h5);
+  ASSERT_TRUE(res5);
+  EXPECT_EQ(304, res5->status);
+
+  svr.stop();
+  t.join();
+  std::remove(fname);
+}
+
+TEST(ETagTest, VaryAcceptEncodingWithCompression) {
+  using namespace httplib;
+
+  Server svr;
+
+  // Endpoint that returns compressible content
+  svr.Get("/compressible", [](const Request &, Response &res) {
+    // Return a large enough body to trigger compression
+    std::string body(1000, 'a');
+    res.set_content(body, "text/plain");
+  });
+
+  auto t = std::thread([&]() { svr.listen("localhost", 8089); });
+  svr.wait_until_ready();
+
+  Client cli("localhost", 8089);
+
+  // Request with gzip support: should get Vary header when compressed
+  cli.set_compress(true);
+  auto res1 = cli.Get("/compressible");
+  ASSERT_TRUE(res1);
+  EXPECT_EQ(200, res1->status);
+
+  // If Content-Encoding is set, Vary should also be set
+  if (res1->has_header("Content-Encoding")) {
+    EXPECT_TRUE(res1->has_header("Vary"));
+    EXPECT_EQ("Accept-Encoding", res1->get_header_value("Vary"));
+  }
+
+  // Request without Accept-Encoding header: should not have compression
+  Headers h_no_compress;
+  auto res2 = cli.Get("/compressible", h_no_compress);
+  ASSERT_TRUE(res2);
+  EXPECT_EQ(200, res2->status);
+
+  // Verify Vary header is present when compression is applied
+  // (the exact behavior depends on server configuration)
+
+  svr.stop();
+  t.join();
+}
+
+TEST(ETagTest, IfRangeWithETag) {
+  using namespace httplib;
+
+  // Create a test file with known content
+  const char *fname = "if_range_testfile.txt";
+  const std::string content = "0123456789ABCDEFGHIJ"; // 20 bytes
+  {
+    std::ofstream ofs(fname);
+    ofs << content;
+    ASSERT_TRUE(ofs.good());
+  }
+
+  Server svr;
+  svr.set_mount_point("/static", ".");
+  auto t = std::thread([&]() { svr.listen("localhost", 8090); });
+  svr.wait_until_ready();
+
+  Client cli("localhost", 8090);
+
+  // First request: get ETag
+  auto res1 = cli.Get("/static/if_range_testfile.txt");
+  ASSERT_TRUE(res1);
+  ASSERT_EQ(200, res1->status);
+  ASSERT_TRUE(res1->has_header("ETag"));
+  std::string etag = res1->get_header_value("ETag");
+
+  // RFC 9110 Section 13.1.5: If-Range requires strong ETag comparison.
+  // Since our server generates weak ETags (W/"..."), If-Range with our
+  // ETag should NOT result in partial content - it should return full content.
+  Headers h2 = {{"Range", "bytes=0-4"}, {"If-Range", etag}};
+  auto res2 = cli.Get("/static/if_range_testfile.txt", h2);
+  ASSERT_TRUE(res2);
+  // Weak ETag in If-Range -> full content (200), not partial (206)
+  EXPECT_EQ(200, res2->status);
+  EXPECT_EQ(content, res2->body);
+  EXPECT_FALSE(res2->has_header("Content-Range"));
+
+  // Range request with non-matching If-Range (ETag): should get 200 (full
+  // content)
+  Headers h3 = {{"Range", "bytes=0-4"}, {"If-Range", "W/\"wrong-etag\""}};
+  auto res3 = cli.Get("/static/if_range_testfile.txt", h3);
+  ASSERT_TRUE(res3);
+  EXPECT_EQ(200, res3->status);
+  EXPECT_EQ(content, res3->body);
+  EXPECT_FALSE(res3->has_header("Content-Range"));
+
+  // Range request with strong ETag (hypothetical - our server doesn't generate
+  // strong ETags, but if client sends a strong ETag that doesn't match, it
+  // should return full content)
+  Headers h4 = {{"Range", "bytes=0-4"}, {"If-Range", "\"strong-etag\""}};
+  auto res4 = cli.Get("/static/if_range_testfile.txt", h4);
+  ASSERT_TRUE(res4);
+  EXPECT_EQ(200, res4->status);
+  EXPECT_EQ(content, res4->body);
+  EXPECT_FALSE(res4->has_header("Content-Range"));
+
+  svr.stop();
+  t.join();
+  std::remove(fname);
+}
+
+TEST(ETagTest, IfRangeWithDate) {
+  using namespace httplib;
+
+  // Create a test file
+  const char *fname = "if_range_date_testfile.txt";
+  const std::string content = "ABCDEFGHIJ0123456789"; // 20 bytes
+  {
+    std::ofstream ofs(fname);
+    ofs << content;
+    ASSERT_TRUE(ofs.good());
+  }
+
+  Server svr;
+  svr.set_mount_point("/static", ".");
+  auto t = std::thread([&]() { svr.listen("localhost", 8091); });
+  svr.wait_until_ready();
+
+  Client cli("localhost", 8091);
+
+  // First request: get Last-Modified
+  auto res1 = cli.Get("/static/if_range_date_testfile.txt");
+  ASSERT_TRUE(res1);
+  ASSERT_EQ(200, res1->status);
+  ASSERT_TRUE(res1->has_header("Last-Modified"));
+  std::string last_modified = res1->get_header_value("Last-Modified");
+
+  // Range request with matching If-Range (date): should get 206
+  Headers h2 = {{"Range", "bytes=5-9"}, {"If-Range", last_modified}};
+  auto res2 = cli.Get("/static/if_range_date_testfile.txt", h2);
+  ASSERT_TRUE(res2);
+  EXPECT_EQ(206, res2->status);
+  EXPECT_EQ("FGHIJ", res2->body);
+
+  // Range request with old If-Range date: should get 200 (full content)
+  Headers h3 = {{"Range", "bytes=5-9"},
+                {"If-Range", "Sun, 01 Jan 2000 00:00:00 GMT"}};
+  auto res3 = cli.Get("/static/if_range_date_testfile.txt", h3);
+  ASSERT_TRUE(res3);
+  EXPECT_EQ(200, res3->status);
+  EXPECT_EQ(content, res3->body);
+
+  // Range request with future If-Range date: should get 206
+  Headers h4 = {{"Range", "bytes=0-4"},
+                {"If-Range", "Sun, 01 Jan 2099 00:00:00 GMT"}};
+  auto res4 = cli.Get("/static/if_range_date_testfile.txt", h4);
+  ASSERT_TRUE(res4);
+  EXPECT_EQ(206, res4->status);
+  EXPECT_EQ("ABCDE", res4->body);
+
+  svr.stop();
+  t.join();
+  std::remove(fname);
+}
+TEST(ETagTest, MalformedIfNoneMatchAndWhitespace) {
+  using namespace httplib;
+
+  const char *fname = "etag_malformed.txt";
+  const char *content = "malformed-etag";
+  {
+    std::ofstream ofs(fname);
+    ofs << content;
+    ASSERT_TRUE(ofs.good());
+  }
+
+  Server svr;
+  svr.set_mount_point("/static", ".");
+  auto t = std::thread([&]() { svr.listen("localhost", 8092); });
+  svr.wait_until_ready();
+
+  Client cli("localhost", 8092);
+
+  // baseline: should get 200 and an ETag
+  auto res1 = cli.Get("/static/etag_malformed.txt");
+  ASSERT_TRUE(res1);
+  ASSERT_EQ(200, res1->status);
+  ASSERT_TRUE(res1->has_header("ETag"));
+
+  // Malformed ETag value (missing quotes) should be treated as non-matching
+  Headers h_bad = {{"If-None-Match", "W/noquotes"}};
+  auto res_bad = cli.Get("/static/etag_malformed.txt", h_bad);
+  ASSERT_TRUE(res_bad);
+  EXPECT_EQ(200, res_bad->status);
+
+  // Whitespace-only header value should be considered invalid / non-matching
+  Headers h_space = {{"If-None-Match", "   "}};
+  auto res_space = cli.Get("/static/etag_malformed.txt", h_space);
+  ASSERT_TRUE(res_space);
+  EXPECT_EQ(200, res_space->status);
+
+  svr.stop();
+  t.join();
+  std::remove(fname);
+}
+
+TEST(ETagTest, InvalidIfModifiedSinceAndIfRangeDate) {
+  using namespace httplib;
+
+  const char *fname = "ims_invalid_format.txt";
+  const char *content = "ims-bad-format";
+  {
+    std::ofstream ofs(fname);
+    ofs << content;
+    ASSERT_TRUE(ofs.good());
+  }
+
+  Server svr;
+  svr.set_mount_point("/static", ".");
+  auto t = std::thread([&]() { svr.listen("localhost", 8093); });
+  svr.wait_until_ready();
+
+  Client cli("localhost", 8093);
+
+  auto res1 = cli.Get("/static/ims_invalid_format.txt");
+  ASSERT_TRUE(res1);
+  ASSERT_EQ(200, res1->status);
+  ASSERT_TRUE(res1->has_header("Last-Modified"));
+
+  // If-Modified-Since with invalid format should not result in 304
+  Headers h_bad_date = {{"If-Modified-Since", "not-a-valid-date"}};
+  auto res_bad = cli.Get("/static/ims_invalid_format.txt", h_bad_date);
+  ASSERT_TRUE(res_bad);
+  EXPECT_EQ(200, res_bad->status);
+
+  // If-Range with invalid date format should be treated as mismatch -> full
+  // content (200)
+  Headers h_ifrange_bad = {{"Range", "bytes=0-3"},
+                           {"If-Range", "invalid-date"}};
+  auto res_ifrange = cli.Get("/static/ims_invalid_format.txt", h_ifrange_bad);
+  ASSERT_TRUE(res_ifrange);
+  EXPECT_EQ(200, res_ifrange->status);
+
+  svr.stop();
+  t.join();
+  std::remove(fname);
+}
+
+TEST(ETagTest, IfRangeWithMalformedETag) {
+  using namespace httplib;
+
+  const char *fname = "ifrange_malformed.txt";
+  const std::string content = "0123456789";
+  {
+    std::ofstream ofs(fname);
+    ofs << content;
+    ASSERT_TRUE(ofs.good());
+  }
+
+  Server svr;
+  svr.set_mount_point("/static", ".");
+  auto t = std::thread([&]() { svr.listen("localhost", 8094); });
+  svr.wait_until_ready();
+
+  Client cli("localhost", 8094);
+
+  // First request: get ETag
+  auto res1 = cli.Get("/static/ifrange_malformed.txt");
+  ASSERT_TRUE(res1);
+  ASSERT_EQ(200, res1->status);
+  ASSERT_TRUE(res1->has_header("ETag"));
+
+  // If-Range with malformed ETag (no quotes) should be treated as mismatch ->
+  // full content (200)
+  Headers h_malformed = {{"Range", "bytes=0-4"}, {"If-Range", "W/noquotes"}};
+  auto res2 = cli.Get("/static/ifrange_malformed.txt", h_malformed);
+  ASSERT_TRUE(res2);
+  EXPECT_EQ(200, res2->status);
+  EXPECT_EQ(content, res2->body);
+
+  svr.stop();
+  t.join();
+  std::remove(fname);
+}
+
+TEST(ETagTest, ExtremeLargeDateValues) {
+  using namespace httplib;
+
+  const char *fname = "ims_extreme_date.txt";
+  const char *content = "ims-extreme-date";
+  {
+    std::ofstream ofs(fname);
+    ofs << content;
+    ASSERT_TRUE(ofs.good());
+  }
+
+  Server svr;
+  svr.set_mount_point("/static", ".");
+  auto t = std::thread([&]() { svr.listen("localhost", 8095); });
+  svr.wait_until_ready();
+
+  Client cli("localhost", 8095);
+
+  auto res1 = cli.Get(std::string("/static/") + fname);
+  ASSERT_TRUE(res1);
+  ASSERT_EQ(200, res1->status);
+  ASSERT_TRUE(res1->has_header("Last-Modified"));
+
+  // Extremely large year that may overflow date parsing routines.
+  Headers h_large_date = {
+      {"If-Modified-Since", "Sun, 01 Jan 99999 00:00:00 GMT"}};
+  auto res_bad = cli.Get(std::string("/static/") + fname, h_large_date);
+  ASSERT_TRUE(res_bad);
+  // Expect server to treat this as invalid/mismatch and return full content
+  EXPECT_EQ(200, res_bad->status);
+
+  // If-Range with extremely large date should be treated as mismatch -> full
+  // content (200)
+  Headers h_ifrange_large = {{"Range", "bytes=0-3"},
+                             {"If-Range", "Sun, 01 Jan 99999 00:00:00 GMT"}};
+  auto res_ifrange = cli.Get(std::string("/static/") + fname, h_ifrange_large);
+  ASSERT_TRUE(res_ifrange);
+  EXPECT_EQ(200, res_ifrange->status);
+
+  svr.stop();
+  t.join();
+  std::remove(fname);
+}
+
+TEST(ETagTest, NegativeFileModificationTime) {
+  using namespace httplib;
+
+  const char *fname = "ims_negative_mtime.txt";
+  const std::string content = "negative-mtime";
+  {
+    std::ofstream ofs(fname);
+    ofs << content;
+    ASSERT_TRUE(ofs.good());
+  }
+
+  // Try to set file mtime to a negative value. This may fail on some
+  // platforms/filesystems; if it fails, the test will still verify server
+  // behaves safely by performing a regular conditional request.
+#if defined(__APPLE__) || defined(__linux__)
+  bool set_negative = false;
+  do {
+    struct timeval times[2];
+    // access time: now
+    times[0].tv_sec = time(nullptr);
+    times[0].tv_usec = 0;
+    // modification time: negative (e.g., -1)
+    times[1].tv_sec = -1;
+    times[1].tv_usec = 0;
+    if (utimes(fname, times) == 0) { set_negative = true; }
+  } while (0);
+#else
+  bool set_negative = false;
+#endif
+
+  Server svr;
+  svr.set_mount_point("/static", ".");
+  auto t = std::thread([&]() { svr.listen("localhost", 8096); });
+  svr.wait_until_ready();
+
+  Client cli("localhost", 8096);
+
+  auto res1 = cli.Get(std::string("/static/") + fname);
+  ASSERT_TRUE(res1);
+  ASSERT_EQ(200, res1->status);
+  bool has_last_modified = res1->has_header("Last-Modified");
+  std::string last_modified;
+  if (has_last_modified) {
+    last_modified = res1->get_header_value("Last-Modified");
+  }
+
+  if (set_negative) {
+    // If we successfully set a negative mtime, ensure server returns a
+    // Last-Modified string (may be empty or normalized). Send If-Modified-Since
+    // with an old date and ensure server handles it without crash.
+    Headers h_old = {{"If-Modified-Since", "Sun, 01 Jan 1970 00:00:00 GMT"}};
+    auto res2 = cli.Get(std::string("/static/") + fname, h_old);
+    ASSERT_TRUE(res2);
+    // Behavior may vary; at minimum ensure server responds (200 or 304).
+    EXPECT_TRUE(res2->status == 200 || res2->status == 304);
+  } else {
+    // Could not set negative mtime on this platform; fall back to verifying
+    // that normal invalid/malformed dates are treated safely (non-304).
+    Headers h_bad_date = {
+        {"If-Modified-Since", "Sun, 01 Jan 99999 00:00:00 GMT"}};
+    auto res_bad = cli.Get(std::string("/static/") + fname, h_bad_date);
+    ASSERT_TRUE(res_bad);
+    EXPECT_EQ(200, res_bad->status);
+  }
+
+  svr.stop();
+  t.join();
+  std::remove(fname);
+}