Skip to content

Commit 3fe32b6

Browse files
authored
Send query string verbatim when path encoding is disabled (#2479)
set_path_encode(false) only suppressed encoding of the path part. The query part was always run through normalize_query_string(), which decodes then re-encodes each key/value pair regardless of the flag. That round-trip is lossy for pre-encoded payloads: re-encoding emits sub-delimiters literally (%2C->",", %24->"$", %3B->";", ...) and turns %20 into "+", which a strict RFC 3986 server decodes back as "+" (0x2B) rather than a space (0x20), corrupting binary query data. Honor path_encode_ for the query as well: when disabled, append the caller-supplied query verbatim. Add a regression test asserting on the raw request target, since the server decodes "+" as space and would otherwise mask the difference.
1 parent 0ae9388 commit 3fe32b6

2 files changed

Lines changed: 46 additions & 3 deletions

File tree

httplib.h

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13661,9 +13661,18 @@ inline bool ClientImpl::write_request(Stream &strm, Request &req,
1366113661

1366213662
if (!query_part.empty()) {
1366313663
// Normalize the query string (decode then re-encode) while preserving
13664-
// the original parameter order.
13665-
auto normalized = detail::normalize_query_string(query_part);
13666-
if (!normalized.empty()) { path_with_query += '?' + normalized; }
13664+
// the original parameter order. When path encoding is disabled the
13665+
// caller has supplied an already-encoded target and expects the exact
13666+
// bytes to be sent on the wire, so skip normalization for the query
13667+
// too. Normalizing here would decode-then-re-encode the query and
13668+
// corrupt pre-encoded binary payloads (e.g. turning `%20` into `+`,
13669+
// which a strict RFC 3986 server decodes back as `+`, not a space).
13670+
if (path_encode_) {
13671+
auto normalized = detail::normalize_query_string(query_part);
13672+
if (!normalized.empty()) { path_with_query += '?' + normalized; }
13673+
} else {
13674+
path_with_query += '?' + query_part;
13675+
}
1366713676

1366813677
// Still populate req.params for handlers/users who read them.
1366913678
detail::parse_query_text(query_part, req.params);

test/test.cc

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2929,6 +2929,40 @@ TEST(PathUrlEncodeTest, PathUrlEncode) {
29292929
}
29302930
}
29312931

2932+
TEST(PathUrlEncodeTest, PreEncodedQueryNotReencoded) {
2933+
// When path encoding is disabled the client must transmit the supplied
2934+
// query verbatim. Decoding-then-re-encoding it (the previous behavior)
2935+
// corrupts pre-encoded binary payloads: e.g. `%20` would be turned into
2936+
// `+`, which a strict RFC 3986 server decodes back as `+` (0x2B) rather
2937+
// than a space (0x20). Assert on the raw wire target to catch this.
2938+
Server svr;
2939+
2940+
const std::string expected_target = "/foo?q=a%20b%2Cc%24d%3Bx&a=%00%FF";
2941+
2942+
svr.Get("/foo", [&](const Request &req, Response &res) {
2943+
EXPECT_EQ(expected_target, req.target);
2944+
res.status = StatusCode::OK_200;
2945+
});
2946+
2947+
auto thread = std::thread([&]() { svr.listen(HOST, PORT); });
2948+
auto se = detail::scope_exit([&] {
2949+
svr.stop();
2950+
thread.join();
2951+
ASSERT_FALSE(svr.is_running());
2952+
});
2953+
2954+
svr.wait_until_ready();
2955+
2956+
{
2957+
Client cli(HOST, PORT);
2958+
cli.set_path_encode(false);
2959+
2960+
auto res = cli.Get(expected_target.c_str());
2961+
ASSERT_TRUE(res);
2962+
EXPECT_EQ(StatusCode::OK_200, res->status);
2963+
}
2964+
}
2965+
29322966
TEST(PathUrlEncodeTest, IncludePercentEncodingLF) {
29332967
Server svr;
29342968

0 commit comments

Comments
 (0)