@@ -5717,6 +5717,38 @@ inline void parse_query_text(const std::string &s, Params ¶ms) {
57175717 parse_query_text (s.data (), s.size (), params);
57185718}
57195719
5720+ // Normalize a query string by decoding and re-encoding each key/value pair
5721+ // while preserving the original parameter order. This avoids double-encoding
5722+ // and ensures consistent encoding without reordering (unlike Params which
5723+ // uses std::multimap and sorts keys).
5724+ inline std::string normalize_query_string (const std::string &query) {
5725+ std::string result;
5726+ split (query.data (), query.data () + query.size (), ' &' ,
5727+ [&](const char *b, const char *e) {
5728+ std::string key;
5729+ std::string val;
5730+ divide (b, static_cast <std::size_t >(e - b), ' =' ,
5731+ [&](const char *lhs_data, std::size_t lhs_size,
5732+ const char *rhs_data, std::size_t rhs_size) {
5733+ key.assign (lhs_data, lhs_size);
5734+ val.assign (rhs_data, rhs_size);
5735+ });
5736+
5737+ if (!key.empty ()) {
5738+ auto dec_key = decode_query_component (key);
5739+ auto dec_val = decode_query_component (val);
5740+
5741+ if (!result.empty ()) { result += ' &' ; }
5742+ result += encode_query_component (dec_key);
5743+ if (!val.empty () || std::find (b, e, ' =' ) != e) {
5744+ result += ' =' ;
5745+ result += encode_query_component (dec_val);
5746+ }
5747+ }
5748+ });
5749+ return result;
5750+ }
5751+
57205752inline bool parse_multipart_boundary (const std::string &content_type,
57215753 std::string &boundary) {
57225754 auto boundary_keyword = " boundary=" ;
@@ -10204,13 +10236,32 @@ inline bool ClientImpl::write_request(Stream &strm, Request &req,
1020410236 query_part = " " ;
1020510237 }
1020610238
10207- // Encode path and query
10239+ // Encode path part. If the original `req.path` already contained a
10240+ // query component, preserve its raw query string (including parameter
10241+ // order) instead of reparsing and reassembling it which may reorder
10242+ // parameters due to container ordering (e.g. `Params` uses
10243+ // `std::multimap`). When there is no query in `req.path`, fall back to
10244+ // building a query from `req.params` so existing callers that pass
10245+ // `Params` continue to work.
1020810246 auto path_with_query =
1020910247 path_encode_ ? detail::encode_path (path_part) : path_part;
1021010248
10211- detail::parse_query_text (query_part, req.params );
10212- if (!req.params .empty ()) {
10213- path_with_query = append_query_params (path_with_query, req.params );
10249+ if (!query_part.empty ()) {
10250+ // Normalize the query string (decode then re-encode) while preserving
10251+ // the original parameter order.
10252+ auto normalized = detail::normalize_query_string (query_part);
10253+ if (!normalized.empty ()) { path_with_query += ' ?' + normalized; }
10254+
10255+ // Still populate req.params for handlers/users who read them.
10256+ detail::parse_query_text (query_part, req.params );
10257+ } else {
10258+ // No query in path; parse any query_part (empty) and append params
10259+ // from `req.params` when present (preserves prior behavior for
10260+ // callers who provide Params separately).
10261+ detail::parse_query_text (query_part, req.params );
10262+ if (!req.params .empty ()) {
10263+ path_with_query = append_query_params (path_with_query, req.params );
10264+ }
1021410265 }
1021510266
1021610267 // Write request line and headers
0 commit comments