From 5db950e1e18498b43d2805f43db3c8711eef41ef Mon Sep 17 00:00:00 2001 From: yhirose Date: Thu, 22 Oct 2026 10:21:06 -0400 Subject: [PATCH] Normalize the extension of a user MIME type mapping when it is set set_file_extension_and_mimetype_mapping() kept the extension as given, and a file's extension was looked up as it was, then in lowercase, so a mapping given as ".pdf" never matched and one given as "PDF" matched only files ending in ".PDF". The extension is now lowercased and loses a leading '.' when it is set, as Apache's AddType does, and a file's extension is looked up in lowercase only. --- README.md | 2 +- httplib.h | 14 +++++++------- test/test.cc | 26 ++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 85afb62d..8d9c001a 100644 --- a/README.md +++ b/README.md @@ -415,7 +415,7 @@ svr.set_file_extension_and_mimetype_mapping("cpp", "text/x-c"); svr.set_file_extension_and_mimetype_mapping("hh", "text/x-h"); ``` -Extensions match regardless of case, so `photo.JPG` is served as `image/jpeg`. A user mapping is looked up by the exact extension first, then by the extension in lowercase. +Extensions match regardless of case, so `photo.JPG` is served as `image/jpeg`. The extension of a user mapping can be given in any case, with or without a leading `.`. The following are built-in mappings: diff --git a/httplib.h b/httplib.h index 7eec7a85..b78c9463 100644 --- a/httplib.h +++ b/httplib.h @@ -7748,13 +7748,10 @@ inline std::string find_content_type(const std::string &path, const std::map &user_data, const std::string &default_content_type) { - auto ext = file_extension(path); + // Extensions are matched regardless of case, as web servers do + auto ext = case_ignore::to_lower(file_extension(path)); - // Extensions are matched regardless of case, as web servers do, with an - // exact match to a user mapping first - auto lower_ext = case_ignore::to_lower(ext); auto it = user_data.find(ext); - if (it == user_data.end()) { it = user_data.find(lower_ext); } if (it != user_data.end()) { return it->second; } static const struct { @@ -7813,7 +7810,7 @@ find_content_type(const std::string &path, }; for (const auto &t : types) { - if (lower_ext == t.ext) { return t.type; } + if (ext == t.ext) { return t.type; } } return default_content_type; } @@ -13736,7 +13733,10 @@ inline bool Server::remove_mount_point(const std::string &mount_point) { inline Server & Server::set_file_extension_and_mimetype_mapping(const std::string &ext, const std::string &mime) { - file_extension_and_mimetype_map_[ext] = mime; + // Keyed as find_content_type() looks it up + auto key = detail::case_ignore::to_lower(ext); + if (!key.empty() && key[0] == '.') { key.erase(0, 1); } + file_extension_and_mimetype_map_[key] = mime; return *this; } diff --git a/test/test.cc b/test/test.cc index 8d34661f..d1d0fec2 100644 --- a/test/test.cc +++ b/test/test.cc @@ -12641,6 +12641,32 @@ TEST(MountTest, AliasedPathDoesNotPassPreRoutingGuard) { } } +TEST(UserDefinedMIMETypeMappingTest, IgnoresCaseAndLeadingDot) { + Server svr; + svr.set_mount_point("/", "./www"); + svr.set_file_extension_and_mimetype_mapping(".abcde", "text/abcde"); + svr.set_file_extension_and_mimetype_mapping("HTML", "text/x-html"); + + auto port = svr.bind_to_any_port(HOST); + ASSERT_GT(port, 0); + thread t = thread([&] { svr.listen_after_bind(); }); + auto se = detail::scope_exit([&] { + svr.stop(); + t.join(); + ASSERT_FALSE(svr.is_running()); + }); + svr.wait_until_ready(); + + Client cli(HOST, port); + auto res = cli.Get("/dir/test.abcde"); + ASSERT_TRUE(res) << "Error: " << to_string(res.error()); + EXPECT_EQ("text/abcde", res->get_header_value("Content-Type")); + + res = cli.Get("/dir/test.html"); + ASSERT_TRUE(res) << "Error: " << to_string(res.error()); + EXPECT_EQ("text/x-html", res->get_header_value("Content-Type")); +} + #ifdef _WIN32 // Issue #2435: mmap::open() must succeed even when another handle holds // the file open for writing (e.g. an active log file).