This is an automated email from the ASF dual-hosted git repository.

wwbmmm pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brpc.git


The following commit(s) were added to refs/heads/master by this push:
     new 023dc4f8 Bound the number of http headers and query parameters per 
message (#3524)
023dc4f8 is described below

commit 023dc4f8bda4a888004fe2ed2a17ab9940ab6d44
Author: Bright Chen <[email protected]>
AuthorDate: Tue Sep 15 16:54:34 2026 +0800

    Bound the number of http headers and query parameters per message (#3524)
---
 src/brpc/details/http_message.cpp        |  11 +
 src/brpc/policy/http2_rpc_protocol.cpp   |  62 ++++-
 src/brpc/policy/http2_rpc_protocol.h     |  17 +-
 src/brpc/socket.cpp                      |   1 +
 src/brpc/uri.cpp                         |  40 +++-
 src/brpc/uri.h                           |   7 +-
 test/brpc_http_message_unittest.cpp      |  60 +++++
 test/brpc_http_rpc_protocol_unittest.cpp | 393 +++++++++++++++++++++++++------
 test/brpc_uri_unittest.cpp               |  32 +++
 9 files changed, 522 insertions(+), 101 deletions(-)

diff --git a/src/brpc/details/http_message.cpp 
b/src/brpc/details/http_message.cpp
index 0cb4f783..14a81e55 100644
--- a/src/brpc/details/http_message.cpp
+++ b/src/brpc/details/http_message.cpp
@@ -49,6 +49,9 @@ DEFINE_int32(http_verbose_max_body_length, 512,
 DEFINE_bool(http_check_outbound_header_crlf, true,
             "Skip outbound http header fields whose name or value contains "
             "CR/LF to prevent request/response splitting.");
+DEFINE_uint32(http_max_header_count, 100,
+              "Reject a message carrying more than so many header fields. "
+              "0 lifts the limit.");
 DECLARE_int64(socket_max_unwritten_bytes);
 DECLARE_uint64(max_body_size);
 
@@ -131,6 +134,14 @@ int HttpMessage::on_header_value(http_parser *parser,
             http_message->_cur_value =
                 &header.AddHeader(http_message->_cur_header);
         }
+
+        if (FLAGS_http_max_header_count > 0 &&
+            header.HeaderCount() > FLAGS_http_max_header_count) {
+            LOG(ERROR) << "Too many headers, max="
+                       << FLAGS_http_max_header_count;
+            return -1;
+        }
+
         if (http_message->_cur_value && !http_message->_cur_value->empty()) {
             http_message->_cur_value->append(
                 header.HeaderValueDelimiter(http_message->_cur_header));
diff --git a/src/brpc/policy/http2_rpc_protocol.cpp 
b/src/brpc/policy/http2_rpc_protocol.cpp
index b20a0572..6d9b2b87 100644
--- a/src/brpc/policy/http2_rpc_protocol.cpp
+++ b/src/brpc/policy/http2_rpc_protocol.cpp
@@ -29,6 +29,7 @@ DECLARE_int32(http_verbose_max_body_length);
 DECLARE_int32(health_check_interval);
 DECLARE_bool(usercode_in_pthread);
 DECLARE_int64(socket_max_unwritten_bytes);
+DECLARE_uint32(http_max_header_count);
 
 namespace policy {
 
@@ -729,6 +730,11 @@ H2ParseResult H2StreamContext::OnHeaders(
                 << ", stream_id=" << frame_head.stream_id;
             return MakeH2Error(H2_PROTOCOL_ERROR);
         }
+        // The whole block went through the decoder, the connection is in a
+        // consistent state again and only this stream needs to be reset.
+        if (_rejected_error != H2_NO_ERROR) {
+            return MakeH2Error(_rejected_error, stream_id());
+        }
         if (frame_head.flags & H2_FLAGS_END_STREAM) {
             return OnEndStream();
         }
@@ -791,6 +797,10 @@ H2ParseResult H2StreamContext::OnContinuation(
                 << ", stream_id=" << frame_head.stream_id;
             return MakeH2Error(H2_PROTOCOL_ERROR);
         }
+        // See the same check in H2StreamContext::OnHeaders().
+        if (_rejected_error != H2_NO_ERROR) {
+            return MakeH2Error(_rejected_error, stream_id());
+        }
         if (_stream_ended) {
             return OnEndStream();
         }
@@ -1294,7 +1304,8 @@ H2StreamContext::H2StreamContext(bool 
read_body_progressively)
     , _remote_window_left(0)
     , _deferred_window_update(0)
     , _correlation_id(INVALID_BTHREAD_ID.value)
-    , _decoded_header_list_size(0) {
+    , _decoded_header_list_size(0)
+    , _rejected_error(H2_NO_ERROR) {
     header().set_version(2, 0);
 #ifndef NDEBUG
     get_h2_bvars()->h2_stream_context_count << 1;
@@ -1349,6 +1360,14 @@ int 
H2StreamContext::ConsumeHeaders(butil::IOBufBytesIterator& it) {
                        << max_header_list_size << ", stream_id=" << _stream_id;
             return -1;
         }
+        if (_rejected_error != H2_NO_ERROR) {
+            // The stream is already refused, keep feeding the decoder so that
+            // the dynamic table stays in sync with the peer, but stop spending
+            // memory on fields nobody is going to read. A peer that keeps
+            // piling them up still runs into max_header_list_size above, which
+            // escalates to a connection error as it has to.
+            continue;
+        }
         const char* const name = pair.name.c_str();
         bool matched = false;
         if (name[0] == ':') { // reserved names
@@ -1364,10 +1383,12 @@ int 
H2StreamContext::ConsumeHeaders(butil::IOBufBytesIterator& it) {
                     matched = true;
                     HttpMethod method;
                     if (!Str2HttpMethod(pair.value.c_str(), &method)) {
-                        LOG(ERROR) << "Invalid method=" << pair.value;
-                        return -1;
+                        LOG(ERROR) << "Invalid method=" << pair.value
+                                   << ", stream_id=" << _stream_id;
+                        _rejected_error = H2_PROTOCOL_ERROR;
+                    } else {
+                        h.set_method(method);
                     }
-                    h.set_method(method);
                 }
                 break;
             case 'p':
@@ -1380,11 +1401,16 @@ int 
H2StreamContext::ConsumeHeaders(butil::IOBufBytesIterator& it) {
                     // would take the whole header block, since HPACK does not
                     // order pseudo-headers and :method may not have arrived.
                     if (pair.value != "*" && (pair.value.empty() || 
pair.value[0] != '/')) {
-                        LOG(ERROR) << "Invalid path=" << pair.value;
-                        return -1;
+                        LOG(ERROR) << "Invalid path=" << pair.value
+                                   << ", stream_id=" << _stream_id;
+                        _rejected_error = H2_PROTOCOL_ERROR;
+                    } else if (h.uri().SetH2Path(pair.value) != 0) {
+                        // Including path/query/fragment. The only way this
+                        // fails is too many query parameters.
+                        LOG(ERROR) << h.uri().status().error_cstr()
+                                   << ", stream_id=" << _stream_id;
+                        _rejected_error = H2_ENHANCE_YOUR_CALM;
                     }
-                    // Including path/query/fragment
-                    h.uri().SetH2Path(pair.value);
                 }
                 break;
             case 's':
@@ -1396,24 +1422,34 @@ int 
H2StreamContext::ConsumeHeaders(butil::IOBufBytesIterator& it) {
                     char* endptr = nullptr;
                     const int sc = strtol(pair.value.c_str(), &endptr, 10);
                     if (*endptr != '\0') {
-                        LOG(ERROR) << "Invalid status=" << pair.value;
-                        return -1;
+                        LOG(ERROR) << "Invalid status=" << pair.value
+                                   << ", stream_id=" << _stream_id;
+                        _rejected_error = H2_PROTOCOL_ERROR;
+                    } else {
+                        h.set_status_code(sc);
                     }
-                    h.set_status_code(sc);
                 }
                 break;
             default:
                 break;
             }
             if (!matched) {
-                LOG(ERROR) << "Unknown name=`" << name << '\'';
-                return -1;
+                LOG(ERROR) << "Unknown pseudo-header=`" << name
+                           << "', stream_id=" << _stream_id;
+                _rejected_error = H2_PROTOCOL_ERROR;
             }
         } else if (name[0] == 'c' &&
                    strcmp(name + 1, /*c*/"ontent-type") == 0) {
             h.set_content_type(pair.value);
         } else {
             h.AppendHeader(pair.name, pair.value);
+            if (FLAGS_http_max_header_count > 0 &&
+                h.HeaderCount() > FLAGS_http_max_header_count) {
+                LOG(ERROR) << "Too many headers, max="
+                           << FLAGS_http_max_header_count
+                           << ", stream_id=" << _stream_id;
+                _rejected_error = H2_ENHANCE_YOUR_CALM;
+            }
         }
 
         if (FLAGS_http_verbose) {
diff --git a/src/brpc/policy/http2_rpc_protocol.h 
b/src/brpc/policy/http2_rpc_protocol.h
index 6c58f71a..64c9864d 100644
--- a/src/brpc/policy/http2_rpc_protocol.h
+++ b/src/brpc/policy/http2_rpc_protocol.h
@@ -234,7 +234,9 @@ public:
 
     // Decode headers in HPACK from *it and set into this->header(). The input
     // does not need to complete.
-    // Returns 0 on success, -1 otherwise.
+    // Returns 0 on success, -1 on a connection-level error. A message that is
+    // merely malformed or unacceptable does not fail here, it sets
+    // `_rejected_error` instead, see the comment on that field.
     int ConsumeHeaders(butil::IOBufBytesIterator& it);
     H2ParseResult OnEndStream();
 
@@ -281,6 +283,19 @@ friend class H2Context;
     // (name + value + 32 per field, RFC 7540 section 10.5.1), checked
     // against the local max_header_list_size in ConsumeHeaders().
     uint64_t _decoded_header_list_size;
+    // Set when this message must be refused although the connection itself is
+    // still healthy: it is malformed (invalid or unknown pseudo-header, RFC
+    // 9113 section 8.1.1 mandates a stream error of type PROTOCOL_ERROR) or it
+    // violates a local limit (too many headers, too many query parameters in
+    // :path). Only the stream is reset so that the other streams keep working,
+    // but the error cannot be raised where it is detected: HPACK keeps a
+    // dynamic table per connection, so leaving the rest of the block undecoded
+    // would desynchronize it from the encoding table of the peer and corrupt
+    // every header block that follows. RFC 9113 section 10.5.1: "The field
+    // block MUST be processed to ensure a consistent connection state, unless
+    // the connection is closed." Hence the rejection is remembered here and
+    // turned into a RST_STREAM once END_HEADERS is reached.
+    H2Error _rejected_error;
     butil::IOBuf _remaining_header_fragment;
     // Request body which cannot be sent yet due to remote flow control.
     // Accessed under H2Context::_stream_mutex.
diff --git a/src/brpc/socket.cpp b/src/brpc/socket.cpp
index 283473df..a0b49662 100644
--- a/src/brpc/socket.cpp
+++ b/src/brpc/socket.cpp
@@ -794,6 +794,7 @@ int Socket::OnCreated(const SocketOptions& options) {
     _unwritten_bytes.store(0, butil::memory_order_relaxed);
     _keepalive_options = options.keepalive_options;
     _tcp_user_timeout_ms = options.tcp_user_timeout_ms;
+    _http_request_method = HTTP_METHOD_GET;
     CHECK(nullptr == _write_head.load(butil::memory_order_relaxed));
     _is_write_shutdown = false;
     int fd = options.fd;
diff --git a/src/brpc/uri.cpp b/src/brpc/uri.cpp
index 2881a8e5..6b10464b 100644
--- a/src/brpc/uri.cpp
+++ b/src/brpc/uri.cpp
@@ -17,9 +17,8 @@
 
 
 #include <ctype.h>                         // isalnum
-
 #include <unordered_set>
-
+#include <gflags/gflags.h>
 #include "brpc/log.h"
 #include "brpc/details/http_parser.h"      // http_parser_parse_url
 #include "brpc/uri.h"                      // URI
@@ -27,15 +26,16 @@
 
 namespace brpc {
 
+DEFINE_uint32(http_max_query_count, 1000,
+              "Reject a URL carrying more than so many query parameters. "
+              "0 lifts the limit.");
+
 URI::URI() 
     : _port(-1)
     , _query_was_modified(false)
     , _initialized_query_map(false)
 {}
 
-URI::~URI() {
-}
-
 void URI::Clear() {
     _st.reset();
     _port = -1;
@@ -64,6 +64,22 @@ void URI::Swap(URI &rhs) {
     _query_map.swap(rhs._query_map);
 }
 
+// Counting separators rather than map entries deliberately overestimates: the
+// splitter walks every segment even when the keys repeat, and it is that walk,
+// not the final map size, that the limit is meant to bound.
+static bool TooManyQueries(const std::string& query) {
+    if (FLAGS_http_max_query_count == 0 || query.empty()) {
+        return false;
+    }
+    uint32_t count = 1;
+    for (char i : query) {
+        if (i == '&' && ++count > FLAGS_http_max_query_count) {
+            return true;
+        }
+    }
+    return false;
+}
+
 // Parse queries, which is case-sensitive
 static void ParseQueries(URI::QueryMap& query_map, const std::string &query) {
     query_map.clear();
@@ -238,6 +254,11 @@ int URI::SetHttpURL(const char* url) {
             }
         }
         _query.assign(start, p - start);
+        if (TooManyQueries(_query)) {
+            _st.set_error(EINVAL, "More than %u query parameters in url",
+                          FLAGS_http_max_query_count);
+            return -1;
+        }
     }
     if (*p == '#') {
         start = ++p;
@@ -411,7 +432,8 @@ void URI::SetHostAndPort(const std::string& host) {
     _host.assign(host_begin, host_end - host_begin);
 }
 
-void URI::SetH2Path(const char* h2_path) {
+int URI::SetH2Path(const char* h2_path) {
+    _st.reset();
     _path.clear();
     _query.clear();
     _fragment.clear();
@@ -427,12 +449,18 @@ void URI::SetH2Path(const char* h2_path) {
         start = ++p;
         for (; *p && *p != '#'; ++p) {}
         _query.assign(start, p - start);
+        if (TooManyQueries(_query)) {
+            _st.set_error(EINVAL, "More than %u query parameters in :path",
+                          FLAGS_http_max_query_count);
+            return -1;
+        }
     }
     if (*p == '#') {
         start = ++p;
         for (; *p; ++p) {}
         _fragment.assign(start, p - start);
     }
+    return 0;
 }
 
 QueryRemover::QueryRemover(const std::string* str)
diff --git a/src/brpc/uri.h b/src/brpc/uri.h
index 7edac400..a42cf88f 100644
--- a/src/brpc/uri.h
+++ b/src/brpc/uri.h
@@ -56,7 +56,7 @@ public:
 
     // You can copy a URI.
     URI();
-    ~URI();
+    ~URI() = default;
 
     // Exchange internal fields with another URI.
     void Swap(URI &rhs);
@@ -99,8 +99,9 @@ public:
     void set_port(int port) { _port = port; }
     void SetHostAndPort(const std::string& host_and_optional_port);
     // Set path/query/fragment with the input in form of "path?query#fragment"
-    void SetH2Path(const char* h2_path);
-    void SetH2Path(const std::string& path) { SetH2Path(path.c_str()); }
+    // Returns 0 on success, -1 otherwise and status() is set.
+    int SetH2Path(const char* h2_path);
+    int SetH2Path(const std::string& path) { return SetH2Path(path.c_str()); }
     
     // Get the value of a CASE-SENSITIVE key.
     // Returns pointer to the value, nullptr when the key does not exist.
diff --git a/test/brpc_http_message_unittest.cpp 
b/test/brpc_http_message_unittest.cpp
index 57e98cca..90c9dbdd 100644
--- a/test/brpc_http_message_unittest.cpp
+++ b/test/brpc_http_message_unittest.cpp
@@ -32,6 +32,7 @@ DECLARE_bool(allow_chunked_length);
 DECLARE_bool(allow_http_1_1_request_without_host);
 DECLARE_bool(http_allow_obs_fold);
 DECLARE_bool(http_strict_header_token);
+DECLARE_uint32(http_max_header_count);
 
 int main(int argc, char* argv[]) {
     testing::InitGoogleTest(&argc, argv);
@@ -643,6 +644,65 @@ TEST(HttpMessageTest, htab_is_ows_in_header_values) {
     }
 }
 
+TEST(HttpMessageTest, too_many_headers) {
+    GFLAGS_NAMESPACE::FlagSaver flag_saver;
+    brpc::FLAGS_http_max_header_count = 8;
+
+    // Host counts as well, so 8 distinct names in total are accepted.
+    std::string at_limit = "GET / HTTP/1.1\r\nHost: a.com\r\n";
+    for (int i = 1; i < 8; ++i) {
+        at_limit.append("h" + std::to_string(i) + ": v\r\n");
+    }
+    std::string over_limit = at_limit + "last: v\r\n\r\n";
+    at_limit.append("\r\n");
+    {
+        brpc::HttpMessage http_message;
+        ASSERT_EQ((ssize_t)at_limit.size(),
+                  http_message.ParseFromArray(at_limit.data(), 
at_limit.size()))
+            << http_message._parser;
+        ASSERT_EQ(8u, http_message.header().HeaderCount());
+    }
+    {
+        brpc::HttpMessage http_message;
+        ASSERT_EQ(-1, http_message.ParseFromArray(over_limit.data(),
+                                                  over_limit.size()));
+    }
+
+    // Repeated names fold into one entry, so they occupy one bucket and are 
not
+    // what the limit is aimed at.
+    std::string folded = "GET / HTTP/1.1\r\nHost: a.com\r\n";
+    for (int i = 0; i < 100; ++i) {
+        folded.append("dup: v\r\n");
+    }
+    folded.append("\r\n");
+    {
+        brpc::HttpMessage http_message;
+        ASSERT_EQ((ssize_t)folded.size(),
+                  http_message.ParseFromArray(folded.data(), folded.size()))
+            << http_message._parser;
+        ASSERT_EQ(2u, http_message.header().HeaderCount());
+    }
+    // Set-Cookie is the one name that does not fold, so each occurrence is its
+    // own entry and does count.
+    std::string cookies = "GET / HTTP/1.1\r\nHost: a.com\r\n";
+    for (int i = 0; i < 100; ++i) {
+        cookies.append("Set-Cookie: a=b\r\n");
+    }
+    cookies.append("\r\n");
+    {
+        brpc::HttpMessage http_message;
+        ASSERT_EQ(-1, http_message.ParseFromArray(cookies.data(), 
cookies.size()));
+    }
+
+    brpc::FLAGS_http_max_header_count = 0;
+    {
+        brpc::HttpMessage http_message;
+        ASSERT_EQ((ssize_t)over_limit.size(),
+                  http_message.ParseFromArray(over_limit.data(), 
over_limit.size()))
+            << http_message._parser;
+    }
+}
+
 TEST(HttpMessageTest, find_method_property_by_uri) {
     brpc::Server server;
     ASSERT_EQ(0, server.AddService(new test::EchoService(),
diff --git a/test/brpc_http_rpc_protocol_unittest.cpp 
b/test/brpc_http_rpc_protocol_unittest.cpp
index 10fb5cc3..136c0f8b 100644
--- a/test/brpc_http_rpc_protocol_unittest.cpp
+++ b/test/brpc_http_rpc_protocol_unittest.cpp
@@ -63,6 +63,8 @@ DECLARE_bool(allow_chunked_length);
 DECLARE_int32(max_connection_pool_size);
 DECLARE_uint64(max_body_size);
 DECLARE_int64(socket_max_unwritten_bytes);
+DECLARE_uint32(http_max_header_count);
+DECLARE_uint32(http_max_query_count);
 extern bvar::CollectorSpeedLimit g_rpc_dump_sl;
 }
 
@@ -727,10 +729,10 @@ TEST_F(HttpTest, complete_flow) {
 }
 
 TEST_F(HttpTest, chunked_uploading) {
-    const int port = 8923;
     brpc::Server server;
-    EXPECT_EQ(0, server.AddService(&_svc, brpc::SERVER_DOESNT_OWN_SERVICE));
-    EXPECT_EQ(0, server.Start(port, nullptr));
+    ASSERT_EQ(0, server.AddService(&_svc, brpc::SERVER_DOESNT_OWN_SERVICE));
+    ASSERT_EQ(0, server.Start(0, nullptr));
+    int port = server.listen_address().port;
 
     // Send request via curl using chunked encoding
     const std::string req = "{\"message\":\"hello\"}";
@@ -889,11 +891,11 @@ private:
 };
     
 TEST_F(HttpTest, read_chunked_response_normally) {
-    const int port = 8923;
     brpc::Server server;
     DownloadServiceImpl svc;
-    EXPECT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
-    EXPECT_EQ(0, server.Start(port, nullptr));
+    ASSERT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
+    ASSERT_EQ(0, server.Start(0, nullptr));
+    int port = server.listen_address().port;
 
     for (int i = 0; i < 3; ++i) {
         svc.set_done_place((DonePlace)i);
@@ -913,11 +915,11 @@ TEST_F(HttpTest, read_chunked_response_normally) {
 }
 
 TEST_F(HttpTest, read_failed_chunked_response) {
-    const int port = 8923;
     brpc::Server server;
     DownloadServiceImpl svc;
-    EXPECT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
-    EXPECT_EQ(0, server.Start(port, nullptr));
+    ASSERT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
+    ASSERT_EQ(0, server.Start(0, nullptr));
+    int port = server.listen_address().port;
 
     brpc::Channel channel;
     brpc::ChannelOptions options;
@@ -1036,10 +1038,10 @@ TEST_F(HttpTest, read_long_body_progressively) {
                             std::numeric_limits<size_t>::max());
     butil::intrusive_ptr<ReadBody> reader;
     {
-        const int port = 8923;
         brpc::Server server;
-        EXPECT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
-        EXPECT_EQ(0, server.Start(port, nullptr));
+        ASSERT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
+        ASSERT_EQ(0, server.Start(0, nullptr));
+        int port = server.listen_address().port;
         {
             brpc::Channel channel;
             brpc::ChannelOptions options;
@@ -1082,12 +1084,12 @@ TEST_F(HttpTest, read_long_body_progressively) {
 
 TEST_F(HttpTest, read_short_body_progressively) {
     butil::intrusive_ptr<ReadBody> reader;
-    const int port = 8923;
     brpc::Server server;
     const int NREP = 10000;
     DownloadServiceImpl svc(DONE_BEFORE_CREATE_PA, NREP);
-    EXPECT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
-    EXPECT_EQ(0, server.Start(port, nullptr));
+    ASSERT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
+    ASSERT_EQ(0, server.Start(0, nullptr));
+    int port = server.listen_address().port;
     {
         brpc::Channel channel;
         brpc::ChannelOptions options;
@@ -1119,11 +1121,11 @@ TEST_F(HttpTest, read_short_body_progressively) {
 }
 
 TEST_F(HttpTest, progressive_read_timeout_keeps_active_reader_alive) {
-    const int port = 8923;
     DownloadServiceImpl svc(DONE_BEFORE_CREATE_PA, 8, 100000);
     brpc::Server server;
     ASSERT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
-    ASSERT_EQ(0, server.Start(port, nullptr));
+    ASSERT_EQ(0, server.Start(0, nullptr));
+    int port = server.listen_address().port;
 
     brpc::Channel channel;
     brpc::ChannelOptions options;
@@ -1148,11 +1150,11 @@ TEST_F(HttpTest, 
progressive_read_timeout_keeps_active_reader_alive) {
 }
 
 TEST_F(HttpTest, progressive_read_timeout_closes_idle_http1_reader_once) {
-    const int port = 8923;
     DownloadServiceImpl svc(DONE_BEFORE_CREATE_PA, 2, 300000);
     brpc::Server server;
     ASSERT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
-    ASSERT_EQ(0, server.Start(port, nullptr));
+    ASSERT_EQ(0, server.Start(0, nullptr));
+    int port = server.listen_address().port;
 
     butil::intrusive_ptr<TimeoutReadBody> reader(new TimeoutReadBody);
     {
@@ -1182,11 +1184,11 @@ TEST_F(HttpTest, 
progressive_read_timeout_closes_idle_http1_reader_once) {
 }
 
 TEST_F(HttpTest, progressive_read_timeout_before_first_body_part) {
-    const int port = 8923;
     DownloadServiceImpl svc(DONE_BEFORE_CREATE_PA, 1, 0, 300000);
     brpc::Server server;
     ASSERT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
-    ASSERT_EQ(0, server.Start(port, nullptr));
+    ASSERT_EQ(0, server.Start(0, nullptr));
+    int port = server.listen_address().port;
 
     butil::intrusive_ptr<TimeoutReadBody> reader(new TimeoutReadBody);
     {
@@ -1215,11 +1217,11 @@ TEST_F(HttpTest, 
progressive_read_timeout_before_first_body_part) {
 }
 
 TEST_F(HttpTest, progressive_read_timeout_ignores_slow_user_callback) {
-    const int port = 8923;
     DownloadServiceImpl svc(DONE_BEFORE_CREATE_PA, 3, 50000);
     brpc::Server server;
     ASSERT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
-    ASSERT_EQ(0, server.Start(port, nullptr));
+    ASSERT_EQ(0, server.Start(0, nullptr));
+    int port = server.listen_address().port;
 
     brpc::Channel channel;
     brpc::ChannelOptions options;
@@ -1245,11 +1247,11 @@ TEST_F(HttpTest, 
progressive_read_timeout_ignores_slow_user_callback) {
 }
 
 TEST_F(HttpTest, progressive_read_timeout_preserves_reader_error) {
-    const int port = 8923;
     DownloadServiceImpl svc(DONE_BEFORE_CREATE_PA, 10);
     brpc::Server server;
     ASSERT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
-    ASSERT_EQ(0, server.Start(port, nullptr));
+    ASSERT_EQ(0, server.Start(0, nullptr));
+    int port = server.listen_address().port;
 
     brpc::Channel channel;
     brpc::ChannelOptions options;
@@ -1274,10 +1276,10 @@ TEST_F(HttpTest, 
progressive_read_timeout_preserves_reader_error) {
 }
 
 TEST_F(HttpTest, progressive_read_timeout_rejects_http2) {
-    const int port = 8923;
     brpc::Server server;
     ASSERT_EQ(0, server.AddService(&_svc, brpc::SERVER_DOESNT_OWN_SERVICE));
-    ASSERT_EQ(0, server.Start(port, nullptr));
+    ASSERT_EQ(0, server.Start(0, nullptr));
+    int port = server.listen_address().port;
 
     brpc::Channel channel;
     brpc::ChannelOptions options;
@@ -1305,10 +1307,10 @@ TEST_F(HttpTest, 
read_progressively_after_cntl_destroys) {
                             std::numeric_limits<size_t>::max());
     butil::intrusive_ptr<ReadBody> reader;
     {
-        const int port = 8923;
         brpc::Server server;
-        EXPECT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
-        EXPECT_EQ(0, server.Start(port, nullptr));
+        ASSERT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
+        ASSERT_EQ(0, server.Start(0, nullptr));
+        int port = server.listen_address().port;
         {
             brpc::Channel channel;
             brpc::ChannelOptions options;
@@ -1351,10 +1353,10 @@ TEST_F(HttpTest, read_progressively_after_long_delay) {
     DownloadServiceImpl svc(DONE_BEFORE_CREATE_PA,
                             std::numeric_limits<size_t>::max());
     {
-        const int port = 8923;
         brpc::Server server;
-        EXPECT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
-        EXPECT_EQ(0, server.Start(port, nullptr));
+        ASSERT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
+        ASSERT_EQ(0, server.Start(0, nullptr));
+        int port = server.listen_address().port;
         {
             brpc::Channel channel;
             brpc::ChannelOptions options;
@@ -1399,10 +1401,10 @@ TEST_F(HttpTest, read_progressively_after_long_delay) {
 TEST_F(HttpTest, skip_progressive_reading) {
     DownloadServiceImpl svc(DONE_BEFORE_CREATE_PA,
                             std::numeric_limits<size_t>::max());
-    const int port = 8923;
     brpc::Server server;
-    EXPECT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
-    EXPECT_EQ(0, server.Start(port, nullptr));
+    ASSERT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
+    ASSERT_EQ(0, server.Start(0, nullptr));
+    int port = server.listen_address().port;
     brpc::Channel channel;
     brpc::ChannelOptions options;
     options.protocol = brpc::PROTOCOL_HTTP;
@@ -1438,12 +1440,12 @@ public:
 };
 
 TEST_F(HttpTest, failed_on_read_one_part) {
-    const int port = 8923;
     brpc::Server server;
     DownloadServiceImpl svc(DONE_BEFORE_CREATE_PA,
                             std::numeric_limits<size_t>::max());
-    EXPECT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
-    EXPECT_EQ(0, server.Start(port, nullptr));
+    ASSERT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
+    ASSERT_EQ(0, server.Start(0, nullptr));
+    int port = server.listen_address().port;
     brpc::Channel channel;
     brpc::ChannelOptions options;
     options.protocol = brpc::PROTOCOL_HTTP;
@@ -1464,12 +1466,12 @@ TEST_F(HttpTest, failed_on_read_one_part) {
 
 TEST_F(HttpTest, broken_socket_stops_progressive_reading) {
     butil::intrusive_ptr<ReadBody> reader;
-    const int port = 8923;
     brpc::Server server;
     DownloadServiceImpl svc(DONE_BEFORE_CREATE_PA,
                             std::numeric_limits<size_t>::max());
-    EXPECT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
-    EXPECT_EQ(0, server.Start(port, nullptr));
+    ASSERT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
+    ASSERT_EQ(0, server.Start(0, nullptr));
+    int port = server.listen_address().port;
         
     brpc::Channel channel;
     brpc::ChannelOptions options;
@@ -1587,14 +1589,14 @@ private:
 };
 
 TEST_F(HttpTest, server_end_read_short_body_progressively) {
-    const int port = 8923;
     brpc::ServiceOptions opt;
     opt.enable_progressive_read = true;
     opt.ownership = brpc::SERVER_DOESNT_OWN_SERVICE;
     UploadServiceImpl upsvc;
     brpc::Server server;
-    EXPECT_EQ(0, server.AddService(&upsvc, opt));
-    EXPECT_EQ(0, server.Start(port, nullptr));
+    ASSERT_EQ(0, server.AddService(&upsvc, opt));
+    ASSERT_EQ(0, server.Start(0, nullptr));
+    int port = server.listen_address().port;
 
     brpc::Channel channel;
     brpc::ChannelOptions options;
@@ -1626,14 +1628,14 @@ TEST_F(HttpTest, 
server_end_read_short_body_progressively) {
 // Fixme!!! Server progressive reader has a heap-use-after-free bug detected 
by ASan.
 // For details, see 
https://github.com/apache/brpc/issues/2145#issuecomment-2329413363
 TEST_F(HttpTest, server_end_read_failed) {
-    const int port = 8923;
     brpc::ServiceOptions opt;
     opt.enable_progressive_read = true;
     opt.ownership = brpc::SERVER_DOESNT_OWN_SERVICE;
     UploadServiceImpl upsvc;
     brpc::Server server;
-    EXPECT_EQ(0, server.AddService(&upsvc, opt));
-    EXPECT_EQ(0, server.Start(port, nullptr));
+    ASSERT_EQ(0, server.AddService(&upsvc, opt));
+    ASSERT_EQ(0, server.Start(0, nullptr));
+    int port = server.listen_address().port;
 
     brpc::Channel channel;
     brpc::ChannelOptions options;
@@ -1664,10 +1666,10 @@ TEST_F(HttpTest, server_end_read_failed) {
 #endif // BUTIL_USE_ASAN
 
 TEST_F(HttpTest, http2_sanity) {
-    const int port = 8923;
     brpc::Server server;
-    EXPECT_EQ(0, server.AddService(&_svc, brpc::SERVER_DOESNT_OWN_SERVICE));
-    EXPECT_EQ(0, server.Start(port, nullptr));
+    ASSERT_EQ(0, server.AddService(&_svc, brpc::SERVER_DOESNT_OWN_SERVICE));
+    ASSERT_EQ(0, server.Start(0, nullptr));
+    int port = server.listen_address().port;
 
     brpc::Channel channel;
     brpc::ChannelOptions options;
@@ -1969,6 +1971,231 @@ TEST_F(HttpTest, 
h2_header_list_budget_resets_per_block) {
     delete sctx;
 }
 
+// Literal header field with a new name, with both lengths in a single 7-bit
+// prefix octet. `first_octet` selects the representation: 0x00 is "without
+// indexing" (RFC 7541 6.2.2), 0x40 is "with incremental indexing" (6.2.1)
+// which also adds the field to the dynamic table.
+// 0x80 of a length octet is the Huffman flag and a length of 128 or more needs
+// the multi-octet form, so refuse what does not fit instead of emitting a
+// corrupt header block.
+void AppendLiteralHeader(butil::IOBuf* out, const std::string& name,
+                         const std::string& value, uint8_t first_octet = 0x00) 
{
+    ASSERT_LT(name.size(), 0x80u);
+    ASSERT_LT(value.size(), 0x80u);
+    uint8_t prefix[] = { first_octet, (uint8_t)name.size() };
+    out->append(prefix, sizeof(prefix));
+    out->append(name);
+    uint8_t value_len = (uint8_t)value.size();
+    out->append(&value_len, 1);
+    out->append(value);
+}
+
+// Feed `payload` to `sctx` as one complete HEADERS block, the way
+// H2Context::Consume() would. A non-zero stream_id in the result means the
+// frame handler asked for a RST_STREAM, a zero one means a GOAWAY that closes
+// the whole connection.
+brpc::policy::H2ParseResult ConsumeHeadersBlock(
+        brpc::policy::H2StreamContext* sctx, const butil::IOBuf& payload,
+        int stream_id) {
+    brpc::policy::H2FrameHead head;
+    head.payload_size = payload.size();
+    head.type = brpc::policy::H2_FRAME_HEADERS;
+    head.flags = 0x4;  // H2_FLAGS_END_HEADERS
+    head.stream_id = stream_id;
+    butil::IOBufBytesIterator it(payload);
+    return sctx->OnHeaders(it, head, payload.size(), 0);
+}
+
+TEST_F(HttpTest, h2_too_many_headers) {
+    GFLAGS_NAMESPACE::FlagSaver flag_saver;
+    brpc::FLAGS_http_max_header_count = 8;
+
+    brpc::policy::H2Context* ctx =
+        new brpc::policy::H2Context(_socket.get(), nullptr);
+    CHECK_EQ(ctx->Init(), 0);
+    _socket->initialize_parsing_context(&ctx);
+
+    {
+        std::unique_ptr<brpc::policy::H2StreamContext> sctx(
+            new brpc::policy::H2StreamContext(false));
+        sctx->Init(ctx, 1);
+        butil::IOBuf payload;
+        for (int i = 0; i < 8; ++i) {
+            AppendLiteralHeader(&payload, "h" + std::to_string(i), "v");
+        }
+        brpc::policy::H2ParseResult res =
+            ConsumeHeadersBlock(sctx.get(), payload, 1);
+        ASSERT_TRUE(res.is_ok()) << brpc::H2ErrorToString(res.error());
+        ASSERT_EQ(8u, sctx->header().HeaderCount());
+    }
+    {
+        std::unique_ptr<brpc::policy::H2StreamContext> sctx(
+            new brpc::policy::H2StreamContext(false));
+        sctx->Init(ctx, 3);
+        butil::IOBuf payload;
+        for (int i = 0; i < 9; ++i) {
+            AppendLiteralHeader(&payload, "h" + std::to_string(i), "v");
+        }
+        // Refusing the request must not cost the connection its other
+        // streams, so the frame handler asks for a RST_STREAM (non-zero
+        // stream_id) rather than a GOAWAY.
+        brpc::policy::H2ParseResult res =
+            ConsumeHeadersBlock(sctx.get(), payload, 3);
+        ASSERT_FALSE(res.is_ok());
+        ASSERT_EQ(brpc::H2_ENHANCE_YOUR_CALM, res.error());
+        ASSERT_EQ(3, res.stream_id());
+    }
+}
+
+TEST_F(HttpTest, h2_too_many_queries_in_path) {
+    GFLAGS_NAMESPACE::FlagSaver flag_saver;
+    brpc::FLAGS_http_max_query_count = 4;
+
+    brpc::policy::H2Context* ctx =
+        new brpc::policy::H2Context(_socket.get(), nullptr);
+    CHECK_EQ(ctx->Init(), 0);
+    _socket->initialize_parsing_context(&ctx);
+
+    {
+        std::unique_ptr<brpc::policy::H2StreamContext> sctx(
+            new brpc::policy::H2StreamContext(false));
+        sctx->Init(ctx, 1);
+        butil::IOBuf payload;
+        AppendLiteralHeader(&payload, ":path", "/s?a=1&b=2&c=3&d=4");
+        brpc::policy::H2ParseResult res =
+            ConsumeHeadersBlock(sctx.get(), payload, 1);
+        ASSERT_TRUE(res.is_ok()) << brpc::H2ErrorToString(res.error());
+    }
+    {
+        std::unique_ptr<brpc::policy::H2StreamContext> sctx(
+            new brpc::policy::H2StreamContext(false));
+        sctx->Init(ctx, 3);
+        butil::IOBuf payload;
+        AppendLiteralHeader(&payload, ":path", "/s?a=1&b=2&c=3&d=4&e=5");
+        brpc::policy::H2ParseResult res =
+            ConsumeHeadersBlock(sctx.get(), payload, 3);
+        ASSERT_FALSE(res.is_ok());
+        ASSERT_EQ(brpc::H2_ENHANCE_YOUR_CALM, res.error());
+        ASSERT_EQ(3, res.stream_id());
+    }
+}
+
+// A refused header block still has to be fed to the HPACK decoder in full.
+// The dynamic table belongs to the connection, so dropping the tail of a block
+// would leave it out of step with the encoding table of the peer and turn
+// every later block into garbage, which is why RFC 9113 section 10.5.1 says
+// the field block MUST be processed unless the connection is closed.
+TEST_F(HttpTest, h2_refused_header_block_keeps_hpack_in_sync) {
+    GFLAGS_NAMESPACE::FlagSaver flag_saver;
+    brpc::FLAGS_http_max_header_count = 2;
+
+    brpc::policy::H2Context* ctx =
+        new brpc::policy::H2Context(_socket.get(), nullptr);
+    CHECK_EQ(ctx->Init(), 0);
+    _socket->initialize_parsing_context(&ctx);
+
+    // Four headers with incremental indexing, two of them past the limit.
+    std::unique_ptr<brpc::policy::H2StreamContext> sctx(
+        new brpc::policy::H2StreamContext(false));
+    sctx->Init(ctx, 1);
+    butil::IOBuf payload;
+    AppendLiteralHeader(&payload, "a", "1", 0x40);
+    AppendLiteralHeader(&payload, "b", "2", 0x40);
+    AppendLiteralHeader(&payload, "c", "3", 0x40);
+    AppendLiteralHeader(&payload, "d", "4", 0x40);
+    brpc::policy::H2ParseResult res =
+        ConsumeHeadersBlock(sctx.get(), payload, 1);
+    ASSERT_FALSE(res.is_ok());
+    ASSERT_EQ(brpc::H2_ENHANCE_YOUR_CALM, res.error());
+    ASSERT_EQ(1, res.stream_id());
+    // Everything after the offending field is decoded but thrown away.
+    ASSERT_EQ(3u, sctx->header().HeaderCount());
+
+    // The static table ends at index 61, so 62 names the newest dynamic entry.
+    // That is "d" only because decoding ran to the end of the block; had it
+    // stopped at the limit, 62 would still be "c".
+    std::unique_ptr<brpc::policy::H2StreamContext> sctx2(
+        new brpc::policy::H2StreamContext(false));
+    sctx2->Init(ctx, 3);
+    butil::IOBuf indexed;
+    const uint8_t indexed_field[] = { 0x80 | 62 };  // Indexed Header Field
+    indexed.append(indexed_field, sizeof(indexed_field));
+    brpc::policy::H2ParseResult res2 =
+        ConsumeHeadersBlock(sctx2.get(), indexed, 3);
+    ASSERT_TRUE(res2.is_ok()) << brpc::H2ErrorToString(res2.error());
+    const std::string* value = sctx2->header().GetHeader("d");
+    ASSERT_TRUE(value != nullptr);
+    ASSERT_EQ("4", *value);
+}
+
+// RFC 9113 section 8.1.1: "Malformed requests or responses that are detected
+// MUST be treated as a stream error (Section 5.4.2) of type PROTOCOL_ERROR."
+// A bad pseudo-header says nothing about the health of the connection, so it
+// must not cost the other streams theirs. :path has its own case table in
+// HttpTest.http2_reject_path_not_starting_with_slash.
+TEST_F(HttpTest, h2_malformed_pseudo_header_resets_stream_only) {
+    struct MalformedField {
+        const char* name;
+        const char* value;
+    };
+    MalformedField malformed[] = {
+        { ":method", "NOSUCH" },
+        { ":status", "20x" },
+        { ":nosuchheader", "1" },  // 8.3: undefined pseudo-header
+    };
+
+    brpc::policy::H2Context* ctx =
+        new brpc::policy::H2Context(_socket.get(), nullptr);
+    CHECK_EQ(ctx->Init(), 0);
+    _socket->initialize_parsing_context(&ctx);
+
+    int stream_id = 1;
+    for (const auto& bad : malformed) {
+        std::unique_ptr<brpc::policy::H2StreamContext> sctx(
+            new brpc::policy::H2StreamContext(false));
+        sctx->Init(ctx, stream_id);
+        butil::IOBuf payload;
+        AppendLiteralHeader(&payload, bad.name, bad.value);
+        brpc::policy::H2ParseResult res =
+            ConsumeHeadersBlock(sctx.get(), payload, stream_id);
+        std::string desc = std::string(bad.name) + '=' + bad.value;
+        ASSERT_FALSE(res.is_ok()) << desc;
+        ASSERT_EQ(brpc::H2_PROTOCOL_ERROR, res.error())
+            << desc << ": " << brpc::H2ErrorToString(res.error());
+        ASSERT_EQ(stream_id, res.stream_id()) << desc;
+        stream_id += 2;
+    }
+
+    // A malformed block is drained like any other refusal, so a field that
+    // follows the bad pseudo-header still reaches the dynamic table. RFC 9113
+    // section 4.3 leaves no choice here: only a decoding error may take down
+    // the connection, so everything else has to be decoded to the end.
+    std::unique_ptr<brpc::policy::H2StreamContext> sctx(
+        new brpc::policy::H2StreamContext(false));
+    sctx->Init(ctx, stream_id);
+    butil::IOBuf payload;
+    AppendLiteralHeader(&payload, ":path", "foo");
+    AppendLiteralHeader(&payload, "after-the-bad-one", "1", 0x40);
+    brpc::policy::H2ParseResult res =
+        ConsumeHeadersBlock(sctx.get(), payload, stream_id);
+    ASSERT_EQ(brpc::H2_PROTOCOL_ERROR, res.error());
+    ASSERT_EQ(stream_id, res.stream_id());
+
+    stream_id += 2;
+    std::unique_ptr<brpc::policy::H2StreamContext> sctx2(
+        new brpc::policy::H2StreamContext(false));
+    sctx2->Init(ctx, stream_id);
+    butil::IOBuf indexed;
+    const uint8_t indexed_field[] = { 0x80 | 62 };  // newest dynamic entry
+    indexed.append(indexed_field, sizeof(indexed_field));
+    brpc::policy::H2ParseResult res2 =
+        ConsumeHeadersBlock(sctx2.get(), indexed, stream_id);
+    ASSERT_TRUE(res2.is_ok()) << brpc::H2ErrorToString(res2.error());
+    const std::string* value = sctx2->header().GetHeader("after-the-bad-one");
+    ASSERT_TRUE(value != nullptr);
+    ASSERT_EQ("1", *value);
+}
+
 TEST_F(HttpTest, h2_oversized_single_headers_block_rejected) {
     // A single HEADERS frame whose decoded header list exceeds
     // max_header_list_size must be rejected at the block boundary (before
@@ -2127,29 +2354,29 @@ TEST_F(HttpTest, http2_invalid_settings) {
         brpc::Server server;
         brpc::ServerOptions options;
         options.h2_settings.stream_window_size = 
brpc::H2Settings::MAX_WINDOW_SIZE + 1;
-        ASSERT_EQ(-1, server.Start("127.0.0.1:8924", &options));
+        ASSERT_EQ(-1, server.Start("127.0.0.1:0", &options));
     }
     {
         brpc::Server server;
         brpc::ServerOptions options;
         options.h2_settings.max_frame_size =
             brpc::H2Settings::DEFAULT_MAX_FRAME_SIZE - 1;
-        ASSERT_EQ(-1, server.Start("127.0.0.1:8924", &options));
+        ASSERT_EQ(-1, server.Start("127.0.0.1:0", &options));
     }
     {
         brpc::Server server;
         brpc::ServerOptions options;
         options.h2_settings.max_frame_size =
             brpc::H2Settings::MAX_OF_MAX_FRAME_SIZE + 1;
-        ASSERT_EQ(-1, server.Start("127.0.0.1:8924", &options));
+        ASSERT_EQ(-1, server.Start("127.0.0.1:0", &options));
     }
 }
 
 TEST_F(HttpTest, http2_not_closing_socket_when_rpc_timeout) {
-    const int port = 8923;
     brpc::Server server;
-    EXPECT_EQ(0, server.AddService(&_svc, brpc::SERVER_DOESNT_OWN_SERVICE));
-    EXPECT_EQ(0, server.Start(port, nullptr));
+    ASSERT_EQ(0, server.AddService(&_svc, brpc::SERVER_DOESNT_OWN_SERVICE));
+    ASSERT_EQ(0, server.Start(0, nullptr));
+    int port = server.listen_address().port;
     brpc::Channel channel;
     brpc::ChannelOptions options;
     options.protocol = "h2";
@@ -2386,7 +2613,8 @@ TEST_F(HttpTest, http2_handle_goaway_streams) {
 }
 
 // RFC 9113 8.3.1: :path MUST NOT be empty and MUST begin with '/', the only
-// exception being the asterisk-form that OPTIONS uses.
+// exception being the asterisk-form that OPTIONS uses. Violating that makes
+// the request malformed, which 8.1.1 turns into a stream error.
 TEST_F(HttpTest, http2_reject_path_not_starting_with_slash) {
     brpc::policy::H2Context* h2_ctx =
         new brpc::policy::H2Context(_socket.get(), &_server);
@@ -2423,23 +2651,32 @@ TEST_F(HttpTest, 
http2_reject_path_not_starting_with_slash) {
         h2_ctx->hpacker().Encode(&appender, header, options);
         butil::IOBuf buf;
         appender.move_to(buf);
-        butil::IOBufBytesIterator it(buf);
 
         brpc::policy::H2StreamContext* h2_msg =
             new brpc::policy::H2StreamContext(false);
         h2_msg->Init(h2_ctx, stream_id);
+        brpc::policy::H2ParseResult res =
+            ConsumeHeadersBlock(h2_msg, buf, stream_id);
+        if (c.accepted) {
+            ASSERT_TRUE(res.is_ok()) << "path=`" << c.path << "': "
+                                     << brpc::H2ErrorToString(res.error());
+        } else {
+            // A bad :path makes the request malformed, not the connection
+            // unusable, so only this stream is reset.
+            ASSERT_EQ(brpc::H2_PROTOCOL_ERROR, res.error())
+                << "path=`" << c.path << '\'';
+            ASSERT_EQ(stream_id, res.stream_id()) << "path=`" << c.path << 
'\'';
+        }
         stream_id += 2;
-        ASSERT_EQ(c.accepted ? 0 : -1, h2_msg->ConsumeHeaders(it))
-            << "path=`" << c.path << '\'';
         h2_msg->Destroy();
     }
 }
 
 TEST_F(HttpTest, spring_protobuf_content_type) {
-    const int port = 8923;
     brpc::Server server;
-    EXPECT_EQ(0, server.AddService(&_svc, brpc::SERVER_DOESNT_OWN_SERVICE));
-    EXPECT_EQ(0, server.Start(port, nullptr));
+    ASSERT_EQ(0, server.AddService(&_svc, brpc::SERVER_DOESNT_OWN_SERVICE));
+    ASSERT_EQ(0, server.Start(0, nullptr));
+    int port = server.listen_address().port;
 
     brpc::Channel channel;
     brpc::ChannelOptions options;
@@ -2483,10 +2720,10 @@ TEST_F(HttpTest, dump_http_request) {
     brpc::g_rpc_dump_sl.sampling_range = bvar::COLLECTOR_SAMPLING_BASE;
 
     // init channel
-    const int port = 8923;
     brpc::Server server;
-    EXPECT_EQ(0, server.AddService(&_svc, brpc::SERVER_DOESNT_OWN_SERVICE));
-    EXPECT_EQ(0, server.Start(port, nullptr));
+    ASSERT_EQ(0, server.AddService(&_svc, brpc::SERVER_DOESNT_OWN_SERVICE));
+    ASSERT_EQ(0, server.Start(0, nullptr));
+    int port = server.listen_address().port;
 
     brpc::Channel channel;
     brpc::ChannelOptions options;
@@ -2555,10 +2792,10 @@ TEST_F(HttpTest, dump_http_request) {
 }
 
 TEST_F(HttpTest, proto_text_content_type) {
-    const int port = 8923;
     brpc::Server server;
-    EXPECT_EQ(0, server.AddService(&_svc, brpc::SERVER_DOESNT_OWN_SERVICE));
-    EXPECT_EQ(0, server.Start(port, nullptr));
+    ASSERT_EQ(0, server.AddService(&_svc, brpc::SERVER_DOESNT_OWN_SERVICE));
+    ASSERT_EQ(0, server.Start(0, nullptr));
+    int port = server.listen_address().port;
 
     brpc::Channel channel;
     brpc::ChannelOptions options;
@@ -2593,10 +2830,10 @@ TEST_F(HttpTest, proto_text_content_type) {
 }
 
 TEST_F(HttpTest, proto_json_content_type) {
-    const int port = 8923;
     brpc::Server server;
-    EXPECT_EQ(0, server.AddService(&_svc, brpc::SERVER_DOESNT_OWN_SERVICE));
-    EXPECT_EQ(0, server.Start(port, nullptr));
+    ASSERT_EQ(0, server.AddService(&_svc, brpc::SERVER_DOESNT_OWN_SERVICE));
+    ASSERT_EQ(0, server.Start(0, nullptr));
+    int port = server.listen_address().port;
 
     brpc::Channel channel;
     brpc::ChannelOptions options;
@@ -2670,11 +2907,11 @@ class HttpServiceImpl : public ::test::HttpService {
 };
 
 TEST_F(HttpTest, http_head) {
-    const int port = 8923;
     brpc::Server server;
     HttpServiceImpl svc;
-    EXPECT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
-    EXPECT_EQ(0, server.Start(port, nullptr));
+    ASSERT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
+    ASSERT_EQ(0, server.Start(0, nullptr));
+    int port = server.listen_address().port;
 
     brpc::Channel channel;
     brpc::ChannelOptions options;
@@ -2798,8 +3035,8 @@ void ReadOneResponse(brpc::SocketUniquePtr& sock,
 TEST_F(HttpTest, http_expect) {
     brpc::Server server;
     HttpServiceImpl svc;
-    EXPECT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
-    EXPECT_EQ(0, server.Start(0, nullptr));
+    ASSERT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
+    ASSERT_EQ(0, server.Start(0, nullptr));
 
     const butil::EndPoint ep = server.listen_address();
     brpc::SocketOptions options;
diff --git a/test/brpc_uri_unittest.cpp b/test/brpc_uri_unittest.cpp
index b9d6b650..b1be2c55 100644
--- a/test/brpc_uri_unittest.cpp
+++ b/test/brpc_uri_unittest.cpp
@@ -15,10 +15,15 @@
 // specific language governing permissions and limitations
 // under the License.
 
+#include <gflags/gflags.h>
 #include <gtest/gtest.h>
 
 #include "brpc/uri.h"
 
+namespace brpc {
+DECLARE_uint32(http_max_query_count);
+}
+
 TEST(URITest, everything) {
     brpc::URI uri;
     std::string uri_str = " 
foobar://user:[email protected]:80/s?wd=uri#frag  ";
@@ -347,6 +352,33 @@ TEST(URITest, invalid_query) {
     ASSERT_EQ("a-b-c:def", uri.query());
 }
 
+TEST(URITest, too_many_queries) {
+    GFLAGS_NAMESPACE::FlagSaver flag_saver;
+    brpc::FLAGS_http_max_query_count = 4;
+
+    brpc::URI uri;
+    ASSERT_EQ(0, uri.SetHttpURL("http://a.com/s?a=1&b=2&c=3&d=4";)) << 
uri.status();
+    ASSERT_EQ(-1, uri.SetHttpURL("http://a.com/s?a=1&b=2&c=3&d=4&e=5";));
+    ASSERT_STREQ("More than 4 query parameters in url", 
uri.status().error_cstr());
+    // Repeated keys collapse into one map entry, but the splitter still walks
+    // every segment, so they count.
+    ASSERT_EQ(-1, uri.SetHttpURL("http://a.com/s?a=1&a=2&a=3&a=4&a=5";));
+    // An empty query is not one parameter.
+    brpc::FLAGS_http_max_query_count = 1;
+    ASSERT_EQ(0, uri.SetHttpURL("http://a.com/s?";)) << uri.status();
+
+    brpc::FLAGS_http_max_query_count = 4;
+    ASSERT_EQ(0, uri.SetH2Path("/s?a=1&b=2&c=3&d=4")) << uri.status();
+    ASSERT_EQ(-1, uri.SetH2Path("/s?a=1&b=2&c=3&d=4&e=5"));
+    ASSERT_STREQ("More than 4 query parameters in :path", 
uri.status().error_cstr());
+    // The next path clears the failure rather than inheriting it.
+    ASSERT_EQ(0, uri.SetH2Path("/s?a=1")) << uri.status();
+
+    brpc::FLAGS_http_max_query_count = 0;
+    ASSERT_EQ(0, uri.SetHttpURL("http://a.com/s?a=1&b=2&c=3&d=4&e=5";)) << 
uri.status();
+    ASSERT_EQ(0, uri.SetH2Path("/s?a=1&b=2&c=3&d=4&e=5")) << uri.status();
+}
+
 TEST(URITest, high_bit_bytes) {
     // Bytes >= 0x80 (e.g. UTF-8 in the host/path) index the +128-biased
     // action table. On unsigned-char platforms they would read past the


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to