Skip to content

Commit 8d6123c

Browse files
committed
Add ETag and Last-Modified handling for If-Range requests
1 parent ce37f8b commit 8d6123c

File tree

2 files changed

+132
-2
lines changed

2 files changed

+132
-2
lines changed

httplib.h

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1257,7 +1257,7 @@ class Server {
12571257
bool listen_internal();
12581258

12591259
bool routing(Request &req, Response &res, Stream &strm);
1260-
bool handle_file_request(const Request &req, Response &res);
1260+
bool handle_file_request(Request &req, Response &res);
12611261
bool dispatch_request(Request &req, Response &res,
12621262
const Handlers &handlers) const;
12631263
bool dispatch_request_for_content_reader(
@@ -3020,6 +3020,12 @@ inline time_t parse_http_date(const std::string &date_str) {
30203020
#endif
30213021
}
30223022

3023+
// Check if the string is an ETag (starts with '"' or 'W/"')
3024+
inline bool is_etag(const std::string &s) {
3025+
return !s.empty() &&
3026+
(s[0] == '"' || (s.size() > 2 && s[0] == 'W' && s[1] == '/'));
3027+
}
3028+
30233029
inline size_t to_utf8(int code, char *buff) {
30243030
if (code < 0x0080) {
30253031
buff[0] = static_cast<char>(code & 0x7F);
@@ -8313,7 +8319,7 @@ inline bool Server::read_content_core(
83138319
return true;
83148320
}
83158321

8316-
inline bool Server::handle_file_request(const Request &req, Response &res) {
8322+
inline bool Server::handle_file_request(Request &req, Response &res) {
83178323
for (const auto &entry : base_dirs_) {
83188324
// Prefix match
83198325
if (!req.path.compare(0, entry.mount_point.size(), entry.mount_point)) {
@@ -8373,6 +8379,30 @@ inline bool Server::handle_file_request(const Request &req, Response &res) {
83738379
}
83748380
}
83758381

8382+
// Handle If-Range for partial content requests (RFC 9110
8383+
// Section 13.1.5) If-Range is only evaluated when Range header is
8384+
// present. If the validator matches, serve partial content; otherwise
8385+
// serve full content.
8386+
if (!req.ranges.empty() && req.has_header("If-Range")) {
8387+
auto if_range = req.get_header_value("If-Range");
8388+
auto valid = false;
8389+
8390+
if (detail::is_etag(if_range)) {
8391+
// ETag comparison (weak comparison for If-Range per RFC 9110)
8392+
valid = (!etag.empty() && if_range == etag);
8393+
} else {
8394+
// HTTP-date comparison
8395+
auto if_range_time = detail::parse_http_date(if_range);
8396+
valid = (if_range_time != static_cast<time_t>(-1) &&
8397+
mtime <= if_range_time);
8398+
}
8399+
8400+
if (!valid) {
8401+
// Validator doesn't match: ignore Range and serve full content
8402+
req.ranges.clear();
8403+
}
8404+
}
8405+
83768406
auto mm = std::make_shared<detail::mmap>(path.c_str());
83778407
if (!mm->is_open()) {
83788408
output_error_log(Error::OpenFile, &req);

test/test.cc

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12848,3 +12848,103 @@ TEST(ETagTest, VaryAcceptEncodingWithCompression) {
1284812848
svr.stop();
1284912849
t.join();
1285012850
}
12851+
12852+
TEST(ETagTest, IfRangeWithETag) {
12853+
using namespace httplib;
12854+
12855+
// Create a test file with known content
12856+
const char *fname = "if_range_testfile.txt";
12857+
const std::string content = "0123456789ABCDEFGHIJ"; // 20 bytes
12858+
{
12859+
std::ofstream ofs(fname);
12860+
ofs << content;
12861+
}
12862+
12863+
Server svr;
12864+
svr.set_mount_point("/static", ".");
12865+
auto t = std::thread([&]() { svr.listen("localhost", 8090); });
12866+
svr.wait_until_ready();
12867+
12868+
Client cli("localhost", 8090);
12869+
12870+
// First request: get ETag
12871+
auto res1 = cli.Get("/static/if_range_testfile.txt");
12872+
ASSERT_TRUE(res1);
12873+
ASSERT_EQ(200, res1->status);
12874+
ASSERT_TRUE(res1->has_header("ETag"));
12875+
std::string etag = res1->get_header_value("ETag");
12876+
12877+
// Range request with matching If-Range (ETag): should get 206
12878+
Headers h2 = {{"Range", "bytes=0-4"}, {"If-Range", etag}};
12879+
auto res2 = cli.Get("/static/if_range_testfile.txt", h2);
12880+
ASSERT_TRUE(res2);
12881+
EXPECT_EQ(206, res2->status);
12882+
EXPECT_EQ("01234", res2->body);
12883+
EXPECT_TRUE(res2->has_header("Content-Range"));
12884+
12885+
// Range request with non-matching If-Range (ETag): should get 200 (full
12886+
// content)
12887+
Headers h3 = {{"Range", "bytes=0-4"}, {"If-Range", "W/\"wrong-etag\""}};
12888+
auto res3 = cli.Get("/static/if_range_testfile.txt", h3);
12889+
ASSERT_TRUE(res3);
12890+
EXPECT_EQ(200, res3->status);
12891+
EXPECT_EQ(content, res3->body);
12892+
EXPECT_FALSE(res3->has_header("Content-Range"));
12893+
12894+
svr.stop();
12895+
t.join();
12896+
std::remove(fname);
12897+
}
12898+
12899+
TEST(ETagTest, IfRangeWithDate) {
12900+
using namespace httplib;
12901+
12902+
// Create a test file
12903+
const char *fname = "if_range_date_testfile.txt";
12904+
const std::string content = "ABCDEFGHIJ0123456789"; // 20 bytes
12905+
{
12906+
std::ofstream ofs(fname);
12907+
ofs << content;
12908+
}
12909+
12910+
Server svr;
12911+
svr.set_mount_point("/static", ".");
12912+
auto t = std::thread([&]() { svr.listen("localhost", 8091); });
12913+
svr.wait_until_ready();
12914+
12915+
Client cli("localhost", 8091);
12916+
12917+
// First request: get Last-Modified
12918+
auto res1 = cli.Get("/static/if_range_date_testfile.txt");
12919+
ASSERT_TRUE(res1);
12920+
ASSERT_EQ(200, res1->status);
12921+
ASSERT_TRUE(res1->has_header("Last-Modified"));
12922+
std::string last_modified = res1->get_header_value("Last-Modified");
12923+
12924+
// Range request with matching If-Range (date): should get 206
12925+
Headers h2 = {{"Range", "bytes=5-9"}, {"If-Range", last_modified}};
12926+
auto res2 = cli.Get("/static/if_range_date_testfile.txt", h2);
12927+
ASSERT_TRUE(res2);
12928+
EXPECT_EQ(206, res2->status);
12929+
EXPECT_EQ("FGHIJ", res2->body);
12930+
12931+
// Range request with old If-Range date: should get 200 (full content)
12932+
Headers h3 = {{"Range", "bytes=5-9"},
12933+
{"If-Range", "Sun, 01 Jan 2000 00:00:00 GMT"}};
12934+
auto res3 = cli.Get("/static/if_range_date_testfile.txt", h3);
12935+
ASSERT_TRUE(res3);
12936+
EXPECT_EQ(200, res3->status);
12937+
EXPECT_EQ(content, res3->body);
12938+
12939+
// Range request with future If-Range date: should get 206
12940+
Headers h4 = {{"Range", "bytes=0-4"},
12941+
{"If-Range", "Sun, 01 Jan 2099 00:00:00 GMT"}};
12942+
auto res4 = cli.Get("/static/if_range_date_testfile.txt", h4);
12943+
ASSERT_TRUE(res4);
12944+
EXPECT_EQ(206, res4->status);
12945+
EXPECT_EQ("ABCDE", res4->body);
12946+
12947+
svr.stop();
12948+
t.join();
12949+
std::remove(fname);
12950+
}

0 commit comments

Comments
 (0)