From be5043a2404e2b65e7c7e32adf728186806fa9de Mon Sep 17 00:00:00 2001 From: yhirose Date: Fri, 16 Oct 2026 23:52:15 -0400 Subject: [PATCH] Replace the Content-Type along with the content set_content_provider() and file content added their Content-Type next to the one an earlier set_content() had set, so the response went out with two, and clients took the first. Each content setter now replaces it. A Content-Type set after set_file_content() is used for the file. --- httplib.h | 25 +++++++++++++++---------- test/test.cc | 31 ++++++++++++++++++++++--------- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/httplib.h b/httplib.h index a49cacbb..c447f301 100644 --- a/httplib.h +++ b/httplib.h @@ -12068,6 +12068,12 @@ inline void clear_content(Response &res) { clear_content_provider(res); } +// The Content-Type of the old content goes with it. +inline void set_content_type(Response &res, const std::string &content_type) { + res.headers.erase("Content-Type"); + res.set_header("Content-Type", content_type); +} + // `body` is public, so it can be assigned directly after a provider or a file // was set. It came last, so it wins. inline void prefer_assigned_body(Response &res) { @@ -12189,10 +12195,7 @@ inline void Response::set_content(const char *s, size_t n, const std::string &content_type) { body.assign(s, n); // Before the releaser runs; it may own `s`. detail::clear_content_provider(*this); - - auto rng = headers.equal_range("Content-Type"); - headers.erase(rng.first, rng.second); - set_header("Content-Type", content_type); + detail::set_content_type(*this, content_type); } inline void Response::set_content(const std::string &s, @@ -12204,18 +12207,15 @@ inline void Response::set_content(std::string &&s, const std::string &content_type) { body = std::move(s); detail::clear_content_provider(*this); - - auto rng = headers.equal_range("Content-Type"); - headers.erase(rng.first, rng.second); - set_header("Content-Type", content_type); + detail::set_content_type(*this, content_type); } inline void Response::set_content_provider( size_t in_length, const std::string &content_type, ContentProvider provider, ContentProviderResourceReleaser resource_releaser) { detail::clear_content(*this); + detail::set_content_type(*this, content_type); - set_header("Content-Type", content_type); content_length_ = in_length; if (in_length > 0) { content_provider_ = std::move(provider); } content_provider_resource_releaser_ = std::move(resource_releaser); @@ -12225,8 +12225,8 @@ inline void Response::set_content_provider( const std::string &content_type, ContentProviderWithoutLength provider, ContentProviderResourceReleaser resource_releaser) { detail::clear_content(*this); + detail::set_content_type(*this, content_type); - set_header("Content-Type", content_type); content_provider_ = detail::ContentProviderAdapter(std::move(provider)); content_provider_resource_releaser_ = std::move(resource_releaser); } @@ -12242,6 +12242,7 @@ inline void Response::set_chunked_content_provider( inline void Response::set_file_content(const std::string &path, const std::string &content_type) { detail::clear_content(*this); + headers.erase("Content-Type"); file_content_path_ = path; file_content_content_type_ = content_type; @@ -15191,6 +15192,10 @@ Server::process_request(Stream &strm, const std::string &remote_addr, file_open_error = true; } else { auto content_type = res.file_content_content_type_; + if (content_type.empty()) { + // One the handler set after set_file_content() + content_type = res.get_header_value("Content-Type"); + } if (content_type.empty()) { content_type = detail::find_content_type( path, file_extension_and_mimetype_map_, default_file_mimetype_); diff --git a/test/test.cc b/test/test.cc index 6b308168..7f6315a4 100644 --- a/test/test.cc +++ b/test/test.cc @@ -13463,12 +13463,20 @@ TEST(ResponseContentSourceTest, LastContentSetterWins) { }); svr.Get("/body-then-provider", [&](const Request &, Response &res) { res.set_content(std::string(100, 'B'), "text/plain"); - res.set_content_provider(3, "text/plain", abc); + res.set_content_provider(3, "text/x-provider", abc); }); svr.Get("/provider-then-body", [&](const Request &, Response &res) { - res.set_content_provider(3, "text/plain", abc); + res.set_content_provider(3, "text/x-provider", abc); res.set_content(std::string(100, 'B'), "text/plain"); }); + svr.Get("/provider-then-file", [&](const Request &, Response &res) { + res.set_content_provider(3, "text/x-provider", abc); + res.set_file_content("./www/dir/test.html", "text/x-file"); + }); + svr.Get("/file-then-header", [](const Request &, Response &res) { + res.set_file_content("./www/dir/test.html"); + res.set_header("Content-Type", "text/x-handler"); + }); svr.Get("/provider-then-direct-body", [&](const Request &, Response &res) { res.set_content_provider(3, "text/plain", abc); res.body = "direct"; @@ -13489,7 +13497,8 @@ TEST(ResponseContentSourceTest, LastContentSetterWins) { Client cli(HOST, port); - auto expect_body = [&](const char *path, const std::string &body) { + auto expect_body = [&](const char *path, const std::string &body, + const std::string &type) { // Uncompressed, so Content-Length can be compared with the body. auto res = cli.Get(path, Headers{{"Accept-Encoding", ""}}); ASSERT_TRUE(res) << path << " Error: " << to_string(res.error()); @@ -13498,14 +13507,18 @@ TEST(ResponseContentSourceTest, LastContentSetterWins) { EXPECT_EQ(std::to_string(body.size()), res->get_header_value("Content-Length")) << path; + EXPECT_EQ(1u, res->get_header_value_count("Content-Type")) << path; + EXPECT_EQ(type, res->get_header_value("Content-Type")) << path; }; - expect_body("/file-then-body", "body"); - expect_body("/body-then-file", "test.html"); - expect_body("/body-then-provider", "abc"); - expect_body("/provider-then-body", std::string(100, 'B')); - expect_body("/provider-then-direct-body", "direct"); - expect_body("/file-then-direct-body", "direct"); + expect_body("/file-then-body", "body", "text/plain"); + expect_body("/body-then-file", "test.html", "text/html"); + expect_body("/body-then-provider", "abc", "text/x-provider"); + expect_body("/provider-then-body", std::string(100, 'B'), "text/plain"); + expect_body("/provider-then-file", "test.html", "text/x-file"); + expect_body("/file-then-header", "test.html", "text/x-handler"); + expect_body("/provider-then-direct-body", "direct", "text/plain"); + expect_body("/file-then-direct-body", "direct", "text/plain"); // Ranges are checked against the body, not the dropped provider. {