From 079f2176af232d5425ad9780b56e202c6401f64a Mon Sep 17 00:00:00 2001 From: yhirose Date: Fri, 16 Oct 2026 21:38:40 -0400 Subject: [PATCH] Put Params before the fragment of a client path append_query_params() added the parameters after a '#', where the fragment, which is not sent, took them along, and write_request() read a query that came after one. Insert them before the fragment, and leave the fragment out of the query. --- httplib.h | 15 +++++++++++---- test/test.cc | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/httplib.h b/httplib.h index 375ca9bb..8628cf70 100644 --- a/httplib.h +++ b/httplib.h @@ -11843,9 +11843,14 @@ inline std::string sanitize_filename(const std::string &filename) { inline std::string append_query_params(const std::string &path, const Params ¶ms) { - std::string path_with_query = path; - path_with_query += path.find('?') == std::string::npos ? '?' : '&'; + // The query ends at the fragment (RFC 3986 3.5), so the params go before it. + auto fragment_pos = path.find('#'); + auto path_with_query = path.substr(0, fragment_pos); + path_with_query += path_with_query.find('?') == std::string::npos ? '?' : '&'; path_with_query += detail::params_to_query_str(params); + if (fragment_pos != std::string::npos) { + path_with_query.append(path, fragment_pos, std::string::npos); + } return path_with_query; } @@ -16370,10 +16375,12 @@ inline bool ClientImpl::write_request(Stream &strm, Request &req, // `encode_request_target`; the raw query is still needed here to decide // between populating `req.params` from it and falling back to building a // query out of caller-supplied `req.params`. - auto query_pos = req.path.find('?'); + // The fragment, which is not sent, is not part of the query. + auto target = req.path.substr(0, req.path.find('#')); + auto query_pos = target.find('?'); auto query_part = query_pos == std::string::npos ? std::string() - : req.path.substr(query_pos + 1); + : target.substr(query_pos + 1); auto path_with_query = detail::encode_request_target(req.path, path_encode_); diff --git a/test/test.cc b/test/test.cc index 43abcd68..5d1ffb81 100644 --- a/test/test.cc +++ b/test/test.cc @@ -5812,6 +5812,44 @@ TEST(URLFragmentTest, WithFragment) { } } +TEST(URLFragmentTest, ParamsGoBeforeTheFragment) { + EXPECT_EQ("/a?b=1#f", append_query_params("/a#f", {{"b", "1"}})); + EXPECT_EQ("/a?q=0&b=1#f", append_query_params("/a?q=0#f", {{"b", "1"}})); + + Server svr; + std::vector targets; + svr.Get("/hi", [&](const Request &req, Response & /*res*/) { + targets.push_back(req.target); + }); + + auto thread = std::thread([&]() { svr.listen(HOST, PORT); }); + auto se = detail::scope_exit([&] { + svr.stop(); + thread.join(); + ASSERT_FALSE(svr.is_running()); + }); + + svr.wait_until_ready(); + + Client cli(HOST, PORT); + EXPECT_TRUE(cli.Get("/hi#f", Params{{"a", "1"}}, Headers{})); + EXPECT_TRUE(cli.Get("/hi?q=0#f", Params{{"a", "1"}}, Headers{})); + EXPECT_TRUE(cli.open_stream("GET", "/hi#f", {{"a", "1"}}).is_valid()); + + // A '?' after the '#' belongs to the fragment, not to the query. + Request req; + req.method = "GET"; + req.path = "/hi#f?x=1"; + req.params.emplace("a", "1"); + Response res; + auto error = Error::Success; + EXPECT_TRUE(cli.send(req, res, error)); + + EXPECT_EQ((std::vector{"/hi?a=1", "/hi?q=0&a=1", "/hi?a=1", + "/hi?a=1"}), + targets); +} + TEST(HeaderWriter, SetHeaderWriter) { Server svr;