diff --git a/httplib.h b/httplib.h index 1b7dd6c8..ac45e408 100644 --- a/httplib.h +++ b/httplib.h @@ -13661,9 +13661,18 @@ inline bool ClientImpl::write_request(Stream &strm, Request &req, if (!query_part.empty()) { // Normalize the query string (decode then re-encode) while preserving - // the original parameter order. - auto normalized = detail::normalize_query_string(query_part); - if (!normalized.empty()) { path_with_query += '?' + normalized; } + // the original parameter order. When path encoding is disabled the + // caller has supplied an already-encoded target and expects the exact + // bytes to be sent on the wire, so skip normalization for the query + // too. Normalizing here would decode-then-re-encode the query and + // corrupt pre-encoded binary payloads (e.g. turning `%20` into `+`, + // which a strict RFC 3986 server decodes back as `+`, not a space). + if (path_encode_) { + auto normalized = detail::normalize_query_string(query_part); + if (!normalized.empty()) { path_with_query += '?' + normalized; } + } else { + path_with_query += '?' + query_part; + } // Still populate req.params for handlers/users who read them. detail::parse_query_text(query_part, req.params); diff --git a/test/test.cc b/test/test.cc index addfc710..156e046a 100644 --- a/test/test.cc +++ b/test/test.cc @@ -2929,6 +2929,40 @@ TEST(PathUrlEncodeTest, PathUrlEncode) { } } +TEST(PathUrlEncodeTest, PreEncodedQueryNotReencoded) { + // When path encoding is disabled the client must transmit the supplied + // query verbatim. Decoding-then-re-encoding it (the previous behavior) + // corrupts pre-encoded binary payloads: e.g. `%20` would be turned into + // `+`, which a strict RFC 3986 server decodes back as `+` (0x2B) rather + // than a space (0x20). Assert on the raw wire target to catch this. + Server svr; + + const std::string expected_target = "/foo?q=a%20b%2Cc%24d%3Bx&a=%00%FF"; + + svr.Get("/foo", [&](const Request &req, Response &res) { + EXPECT_EQ(expected_target, req.target); + res.status = StatusCode::OK_200; + }); + + 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); + cli.set_path_encode(false); + + auto res = cli.Get(expected_target.c_str()); + ASSERT_TRUE(res); + EXPECT_EQ(StatusCode::OK_200, res->status); + } +} + TEST(PathUrlEncodeTest, IncludePercentEncodingLF) { Server svr;