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

wasphin 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 69be4dba5 Support flow-controlled HTTP/2 and gRPC server responses 
(#3690)
69be4dba5 is described below

commit 69be4dba5ad521908933ebf3b7cd43b791c915fc
Author: huangjun <[email protected]>
AuthorDate: Sun Nov 8 23:37:09 2026 +0800

    Support flow-controlled HTTP/2 and gRPC server responses (#3690)
    
    * Support flow-controlled HTTP/2 and gRPC server responses
    
    - Retain server-side H2StreamContext in _pending_streams across OnEndStream 
until the response DATA and trailers are fully flushed or the stream is 
reset/abandoned, using intrusive Stage 2 reference counting.
    - Hold a Socket reference for deferred responses so Server::Stop() drains 
pending DATA/trailers gracefully upon WINDOW_UPDATE, while failing idle 
connections on idle_timeout_sec expiration to clean up buffered responses.
    - Split server response DATA frames according to min(connection_window, 
stream_window, max_frame_size) and resume sending upon WINDOW_UPDATE with 
round-robin stream fairness (_last_flushed_stream_id).
    - Encode gRPC trailer headers via HPacker::EncodeWithoutDynamicTable so 
deferring trailers behind pending DATA never desynchronizes the 
connection-level HPACK dynamic table across concurrent streams.
    - Guard server pending response buffer with -h2_max_pending_data_bytes / 
-socket_max_unwritten_bytes (emitting RST_STREAM(ENHANCE_YOUR_CALM) when 
overcrowded) and release req_body immediately after request deserialization in 
ProcessHttpRequest.
    - Send RST_STREAM(H2_CANCEL) and clean up client stream context when an 
HTTP/2 RPC is canceled or times out.
    - Include RST_STREAM error code and stream_id in client 
Controller::ErrorText while preserving Controller::ErrorCode() == EHTTP.
    - Expose h2_stream_context_count, h2_stream_reset_count, and 
h2_pending_data_bytes bvars in release builds and H2Context::Describe.
    
    * fix: migrate deferred request window update on early H2 response 
completion
    
    ---------
    
    Co-authored-by: huangjun <[email protected]>
---
 src/brpc/details/hpack.cpp               |  23 +
 src/brpc/details/hpack.h                 |   9 +
 src/brpc/policy/http2_rpc_protocol.cpp   | 767 +++++++++++++++++++----------
 src/brpc/policy/http2_rpc_protocol.h     |  84 +++-
 src/brpc/policy/http_rpc_protocol.cpp    |  56 ++-
 src/brpc/socket.cpp                      |  11 +-
 src/brpc/socket.h                        |  16 +
 test/brpc_grpc_protocol_unittest.cpp     | 125 ++++-
 test/brpc_h2_unsent_message_unittest.cpp | 821 ++++++++++++++++++++++++++++++-
 test/brpc_http_rpc_protocol_unittest.cpp | 419 +++++++++++++++-
 10 files changed, 2029 insertions(+), 302 deletions(-)

diff --git a/src/brpc/details/hpack.cpp b/src/brpc/details/hpack.cpp
index 72eced7fe..a4e74b852 100644
--- a/src/brpc/details/hpack.cpp
+++ b/src/brpc/details/hpack.cpp
@@ -748,6 +748,29 @@ void HPacker::Encode(butil::IOBufAppender* out, const 
Header& header,
     EncodeString<false>(out, header.value, options.encode_value);
 }
 
+void HPacker::EncodeWithoutDynamicTable(butil::IOBufAppender* out,
+                                        const Header& header,
+                                        const HPackOptions& options) {
+    CreateStaticTableOnceOrDie();
+    if (options.index_policy != HPACK_NEVER_INDEX_HEADER) {
+        const HeaderAndHashCode hhc = { HeaderHasher()(header), &header };
+        const int index = s_static_table->GetIndexOfHeader(hhc);
+        if (index > 0) {
+            return EncodeInteger(out, 0x80, 7, index);
+        }
+    }
+    const int name_index = s_static_table->GetIndexOfName(header.name);
+    if (options.index_policy == HPACK_NEVER_INDEX_HEADER) {
+        EncodeInteger(out, 0x10, 4, name_index);
+    } else {
+        EncodeInteger(out, 0x00, 4, name_index);
+    }
+    if (name_index == 0) {
+        EncodeString<true>(out, header.name, options.encode_name);
+    }
+    EncodeString<false>(out, header.value, options.encode_value);
+}
+
 inline const HPacker::Header* HPacker::HeaderAt(int index) const {
     return (index >= _decode_table->start_index())
             ? _decode_table->HeaderAt(index) : s_static_table->HeaderAt(index);
diff --git a/src/brpc/details/hpack.h b/src/brpc/details/hpack.h
index bd1da3211..0f3e379c3 100644
--- a/src/brpc/details/hpack.h
+++ b/src/brpc/details/hpack.h
@@ -108,6 +108,15 @@ public:
     void Encode(butil::IOBufAppender* out, const Header& header)
     { return Encode(out, header, HPackOptions()); }
 
+    // Encode header without consulting or mutating the dynamic table (uses
+    // static-table indexed representation when matched, or literal 
representation
+    // per RFC 7541 section 6.1 / 6.2.2 / 6.2.3). Safe to use for trailers 
whose
+    // transmission may be deferred behind flow-controlled DATA frames while
+    // other streams continue to encode HEADERS on the same connection.
+    static void EncodeWithoutDynamicTable(butil::IOBufAppender* out,
+                                          const Header& header,
+                                          const HPackOptions& options);
+
     // Try to decode at most one Header from source and erase corresponding
     // buffer.
     // Returns:
diff --git a/src/brpc/policy/http2_rpc_protocol.cpp 
b/src/brpc/policy/http2_rpc_protocol.cpp
index eb788d5a7..438603bdc 100644
--- a/src/brpc/policy/http2_rpc_protocol.cpp
+++ b/src/brpc/policy/http2_rpc_protocol.cpp
@@ -52,6 +52,15 @@ DEFINE_bool(h2_hpack_encode_name, false,
             "Encode name in HTTP2 headers with huffman encoding");
 DEFINE_bool(h2_hpack_encode_value, false,
             "Encode value in HTTP2 headers with huffman encoding");
+DEFINE_int64(h2_max_pending_data_bytes, 0,
+             "Max bytes of unsent HTTP/2 DATA buffered per connection due to "
+             "remote flow control. 0 means using -socket_max_unwritten_bytes");
+
+inline int64_t GetH2MaxPendingDataBytes() {
+    return FLAGS_h2_max_pending_data_bytes > 0
+        ? FLAGS_h2_max_pending_data_bytes
+        : FLAGS_socket_max_unwritten_bytes;
+}
 
 DEFINE_bool(h2_ack_ignore_eovercrowded, false,
             "Let h2 control-frame replies (PING/SETTINGS acks, RST_STREAM, "
@@ -305,19 +314,6 @@ inline bool AddWindowSize(butil::atomic<int64_t>* 
window_size, int64_t diff) {
     return true;
 }
 
-inline bool MinusWindowSize(butil::atomic<int64_t>* window_size, int64_t size) 
{
-    if (window_size->load(butil::memory_order_relaxed) < size) {
-        // false negative is OK.
-        return false;
-    }
-    int64_t before_sub = window_size->fetch_sub(size, 
butil::memory_order_relaxed);
-    if (before_sub < size) {
-        window_size->fetch_add(size, butil::memory_order_relaxed);
-        return false;
-    }
-    return true;
-}
-
 static H2Context::FrameHandler s_frame_handlers[H2_FRAME_TYPE_MAX + 1];
 static pthread_once_t s_frame_handlers_init_once = PTHREAD_ONCE_INIT;
 void InitFrameHandlers() {
@@ -348,7 +344,10 @@ H2Context::H2Context(Socket* socket, const Server* server)
     , _last_sent_stream_id(1)
     , _goaway_stream_id(-1)
     , _remote_settings_received(false)
+    , _client_preface_pending(false)
+    , _volatile_pending_stream_size(0)
     , _pending_data_size(0)
+    , _last_flushed_stream_id(0)
     , _deferred_window_update(0) {
     // Stop printing the field which is useless for remote settings.
     _remote_settings.connection_window_size = 0;
@@ -379,11 +378,29 @@ H2Context::H2Context(Socket* socket, const Server* server)
 }
 
 H2Context::~H2Context() {
+    SubPendingDataSize(_pending_data_size.load(butil::memory_order_relaxed));
     for (StreamMap::iterator it = _pending_streams.begin();
          it != _pending_streams.end(); ++it) {
-        delete it->second;
+        it->second->RemoveOneRefForStage2();
     }
     _pending_streams.clear();
+    _volatile_pending_stream_size.store(0, butil::memory_order_relaxed);
+}
+
+void H2Context::AddPendingDataSize(size_t n) {
+    if (n > 0) {
+        _pending_data_size.fetch_add(n, butil::memory_order_relaxed);
+        get_h2_bvars()->h2_pending_data_bytes << static_cast<int64_t>(n);
+    }
+}
+
+void H2Context::SubPendingDataSize(size_t n) {
+    if (n > 0) {
+        const size_t prev =
+            _pending_data_size.fetch_sub(n, butil::memory_order_relaxed);
+        CHECK_GE(prev, n);
+        get_h2_bvars()->h2_pending_data_bytes << -static_cast<int64_t>(n);
+    }
 }
 
 int H2Context::Init() {
@@ -402,19 +419,28 @@ H2Settings H2Context::remote_settings() const {
 }
 
 size_t H2Context::VolatilePendingStreamSize() const {
-    std::unique_lock<butil::Mutex> mu(_stream_mutex);
-    return _pending_streams.size();
+    return _volatile_pending_stream_size.load(butil::memory_order_relaxed);
+}
+
+H2StreamContext* H2Context::RemoveStreamLocked(int stream_id) {
+    H2StreamContext* sctx = nullptr;
+    if (!_pending_streams.erase(stream_id, &sctx)) {
+        return nullptr;
+    }
+    _volatile_pending_stream_size.store(
+        _pending_streams.size(), butil::memory_order_relaxed);
+    SubPendingDataSize(sctx->_pending_data.size());
+    return sctx;
 }
 
 H2StreamContext* H2Context::RemoveStreamAndDeferWU(int stream_id) {
     H2StreamContext* sctx = nullptr;
     {
         std::unique_lock<butil::Mutex> mu(_stream_mutex);
-        if (!_pending_streams.erase(stream_id, &sctx)) {
+        sctx = RemoveStreamLocked(stream_id);
+        if (sctx == nullptr) {
             return nullptr;
         }
-        CHECK_GE(_pending_data_size, sctx->_pending_data.size());
-        _pending_data_size -= sctx->_pending_data.size();
     }
     // The remote stream will not send any more data, sending back the
     // stream-level WINDOW_UPDATE is pointless, just move the value into
@@ -432,7 +458,9 @@ void H2Context::RemoveGoAwayStreams(
             std::unique_lock<butil::Mutex> mu(_stream_mutex);
             _goaway_stream_id = goaway_stream_id;
             _pending_streams.swap(tmp);
-            _pending_data_size = 0;
+            _volatile_pending_stream_size.store(0, 
butil::memory_order_relaxed);
+            SubPendingDataSize(
+                _pending_data_size.load(butil::memory_order_relaxed));
         }
         for (StreamMap::const_iterator it = tmp.begin(); it != tmp.end(); 
++it) {
             out_streams->push_back(it->second);
@@ -447,10 +475,7 @@ void H2Context::RemoveGoAwayStreams(
             }
         }
         for (size_t i = 0; i < out_streams->size(); ++i) {
-            CHECK_GE(_pending_data_size,
-                     (*out_streams)[i]->_pending_data.size());
-            _pending_data_size -= (*out_streams)[i]->_pending_data.size();
-            _pending_streams.erase((*out_streams)[i]->stream_id());
+            RemoveStreamLocked((*out_streams)[i]->stream_id());
         }
     }
 }
@@ -464,8 +489,46 @@ H2StreamContext* H2Context::FindStream(int stream_id) {
     return nullptr;
 }
 
+butil::intrusive_ptr<H2StreamContext> H2Context::FindOpenStream(int stream_id) 
{
+    std::unique_lock<butil::Mutex> mu(_stream_mutex);
+    H2StreamContext** psctx = _pending_streams.seek(stream_id);
+    if (psctx == nullptr) {
+        return nullptr;
+    }
+    H2StreamContext* sctx = *psctx;
+    if (is_server_side() &&
+        (sctx->_remote_ended || sctx->_rejected_error != H2_NO_ERROR)) {
+        return nullptr;
+    }
+    // SharedObject's intrusive_ptr_add_ref/intrusive_ptr_release manipulate
+    // the same reference count as AddOneRefForStage2/RemoveOneRefForStage2.
+    return butil::intrusive_ptr<H2StreamContext>(sctx);
+}
+
+butil::intrusive_ptr<H2StreamContext> H2Context::FindHeaderStream(
+    int stream_id) {
+    std::unique_lock<butil::Mutex> mu(_stream_mutex);
+    H2StreamContext** psctx = _pending_streams.seek(stream_id);
+    if (psctx == nullptr) {
+        return nullptr;
+    }
+    H2StreamContext* sctx = *psctx;
+    if (is_server_side() && sctx->_remote_ended &&
+        sctx->_rejected_error == H2_NO_ERROR) {
+        LOG(ERROR) << "Stream already ended remotely, stream_id=" << stream_id;
+        sctx->_rejected_error = H2_STREAM_CLOSED_ERROR;
+        SubPendingDataSize(sctx->_pending_data.size());
+        sctx->_pending_data.clear();
+        sctx->_pending_trailer.clear();
+    }
+    return butil::intrusive_ptr<H2StreamContext>(sctx);
+}
+
 int H2Context::TryToInsertStream(int stream_id, H2StreamContext* ctx) {
     std::unique_lock<butil::Mutex> mu(_stream_mutex);
+    if (is_server_side() && stream_id > _last_received_stream_id) {
+        _last_received_stream_id = stream_id;
+    }
     if (_goaway_stream_id >= 0 && stream_id > _goaway_stream_id) {
         return 1;
     }
@@ -481,10 +544,15 @@ int H2Context::TryToInsertStream(int stream_id, 
H2StreamContext* ctx) {
     }
     H2StreamContext*& sctx = _pending_streams[stream_id];
     if (sctx == nullptr) {
+        if (is_server_side() && stream_id > _last_received_stream_id) {
+            _last_received_stream_id = stream_id;
+        }
         // Synchronize creation with SETTINGS_INITIAL_WINDOW_SIZE updates.
         ctx->_remote_window_left.store(_remote_settings.stream_window_size,
                                        butil::memory_order_relaxed);
         sctx = ctx;
+        _volatile_pending_stream_size.store(
+            _pending_streams.size(), butil::memory_order_relaxed);
         return 0;
     }
     return -1;
@@ -565,6 +633,7 @@ ParseResult H2Context::Consume(
             SerializeFrameHead(rstbuf, 4, H2_FRAME_RST_STREAM,
                                0, h2_res.stream_id());
             SaveUint32(rstbuf + FRAME_HEAD_SIZE, h2_res.error());
+            get_h2_bvars()->h2_stream_reset_count << 1;
             if (WriteAck(_socket, rstbuf, sizeof(rstbuf)) != 0) {
                 LOG(WARNING) << "Fail to send RST_STREAM to " << *_socket;
                 return MakeParseError(PARSE_ERROR_ABSOLUTELY_WRONG);
@@ -572,7 +641,7 @@ ParseResult H2Context::Consume(
             H2StreamContext* sctx = RemoveStreamAndDeferWU(h2_res.stream_id());
             if (sctx) {
                 if (is_server_side()) {
-                    delete sctx;
+                    sctx->RemoveOneRefForStage2();
                     return MakeMessage(nullptr);
                 } else {
                     sctx->header().set_status_code(
@@ -633,6 +702,7 @@ H2ParseResult H2Context::OnHeaders(
         return MakeH2Error(H2_FRAME_SIZE_ERROR);
     }
     frag_size -= pad_length;
+    butil::intrusive_ptr<H2StreamContext> sctx_holder;
     H2StreamContext* sctx = nullptr;
     if (is_server_side() &&
         frame_head.stream_id > _last_received_stream_id) { // new stream
@@ -641,16 +711,15 @@ H2ParseResult H2Context::OnHeaders(
                        << " created by client is not odd";
             return MakeH2Error(H2_PROTOCOL_ERROR);
         }
-        _last_received_stream_id = frame_head.stream_id;
         sctx = new H2StreamContext(_socket->is_read_progressive());
         sctx->Init(this, frame_head.stream_id);
         const int rc = TryToInsertStream(frame_head.stream_id, sctx);
         if (rc < 0) {
-            delete sctx;
+            sctx->RemoveOneRefForStage2();
             LOG(ERROR) << "Fail to insert existing stream_id=" << 
frame_head.stream_id;
             return MakeH2Error(H2_PROTOCOL_ERROR);
         } else if (rc == 2) {
-            delete sctx;
+            sctx->RemoveOneRefForStage2();
             LOG_EVERY_SECOND(WARNING)
                 << "Refused stream_id=" << frame_head.stream_id
                 << " since concurrent streams reached max_concurrent_streams="
@@ -662,11 +731,13 @@ H2ParseResult H2Context::OnHeaders(
             // and REFUSED_STREAM lets a compliant client retry later.
             return MakeH2Error(H2_REFUSED_STREAM, frame_head.stream_id);
         } else if (rc > 0) {
-            delete sctx;
+            sctx->RemoveOneRefForStage2();
             return MakeH2Error(H2_REFUSED_STREAM);
         }
+        sctx_holder.reset(sctx);
     } else {
-        sctx = FindStream(frame_head.stream_id);
+        sctx_holder = FindHeaderStream(frame_head.stream_id);
+        sctx = sctx_holder.get();
         if (sctx == nullptr) {
             if (is_client_side()) {
                 RPC_VLOG << "Fail to find stream_id=" << frame_head.stream_id;
@@ -692,10 +763,12 @@ bool H2StreamContext::HeaderFragmentTooLarge() const {
 H2ParseResult H2StreamContext::OnHeaders(
     butil::IOBufBytesIterator& it, const H2FrameHead& frame_head,
     uint32_t frag_size, uint8_t pad_length) {
-    _parsed_length += FRAME_HEAD_SIZE + frame_head.payload_size;
+    if (_rejected_error == H2_NO_ERROR) {
+        _parsed_length += FRAME_HEAD_SIZE + frame_head.payload_size;
 #if defined(BRPC_H2_STREAM_STATE)
-    SetState(H2_STREAM_OPEN);
+        SetState(H2_STREAM_OPEN);
 #endif
+    }
     // A new HEADERS block (which may be an initial request header set, or
     // trailing headers on the same stream) starts here. The decoded header
     // list budget is per header block (RFC 7540 section 10.5.1), so reset the
@@ -740,7 +813,8 @@ H2ParseResult H2StreamContext::OnHeaders(
         }
         return MakeH2Message(nullptr);
     } else {
-        if (frame_head.flags & H2_FLAGS_END_STREAM) {
+        if (_rejected_error == H2_NO_ERROR &&
+            (frame_head.flags & H2_FLAGS_END_STREAM)) {
             // Delay calling OnEndStream() in OnContinuation()
             _stream_ended = true;
         }
@@ -750,7 +824,8 @@ H2ParseResult H2StreamContext::OnHeaders(
 
 H2ParseResult H2Context::OnContinuation(
     butil::IOBufBytesIterator& it, const H2FrameHead& frame_head) {
-    H2StreamContext* sctx = FindStream(frame_head.stream_id);
+    butil::intrusive_ptr<H2StreamContext> sctx =
+        FindHeaderStream(frame_head.stream_id);
     if (sctx == nullptr) {
         if (is_client_side()) {
             RPC_VLOG << "Fail to find stream_id=" << frame_head.stream_id;
@@ -769,7 +844,9 @@ H2ParseResult H2Context::OnContinuation(
 
 H2ParseResult H2StreamContext::OnContinuation(
     butil::IOBufBytesIterator& it, const H2FrameHead& frame_head) {
-    _parsed_length += FRAME_HEAD_SIZE + frame_head.payload_size;
+    if (_rejected_error == H2_NO_ERROR) {
+        _parsed_length += FRAME_HEAD_SIZE + frame_head.payload_size;
+    }
     it.append_and_forward(&_remaining_header_fragment, 
frame_head.payload_size);
     // A header block may span many CONTINUATION frames; ConsumeHeaders()
     // drains complete fields, so the fragment only buffers one incomplete
@@ -825,7 +902,8 @@ H2ParseResult H2Context::OnData(
         return MakeH2Error(H2_FRAME_SIZE_ERROR);
     }
     frag_size -= pad_length;
-    H2StreamContext* sctx = FindStream(frame_head.stream_id);
+    butil::intrusive_ptr<H2StreamContext> sctx =
+        FindOpenStream(frame_head.stream_id);
     if (sctx == nullptr) {
         // If a DATA frame is received whose stream is not in "open" or 
"half-closed (local)" state,
         // the recipient MUST respond with a stream error (Section 5.4.2) of 
type STREAM_CLOSED.
@@ -835,8 +913,13 @@ H2ParseResult H2Context::OnData(
         tmp_sctx.OnData(it, frame_head, frag_size, pad_length);
         DeferWindowUpdate(tmp_sctx.ReleaseDeferredWindowUpdate());
 
-        LOG(ERROR) << "Fail to find stream_id=" << frame_head.stream_id;
-        return MakeH2Error(H2_STREAM_CLOSED_ERROR, frame_head.stream_id);
+        if (is_server_side()) {
+            RPC_VLOG << "Fail to find stream_id=" << frame_head.stream_id;
+            return MakeH2Error(H2_STREAM_CLOSED_ERROR, frame_head.stream_id);
+        } else {
+            RPC_VLOG << "Fail to find stream_id=" << frame_head.stream_id;
+            return MakeH2Message(nullptr);
+        }
     }
     return sctx->OnData(it, frame_head, frag_size, pad_length);
 }
@@ -904,7 +987,7 @@ H2ParseResult H2Context::OnResetStream(
         return MakeH2Error(H2_FRAME_SIZE_ERROR);
     }
     const H2Error h2_error = static_cast<H2Error>(LoadUint32(it));
-    H2StreamContext* sctx = FindStream(frame_head.stream_id);
+    H2StreamContext* sctx = RemoveStreamAndDeferWU(frame_head.stream_id);
     if (sctx == nullptr) {
         RPC_VLOG << "Fail to find stream_id=" << frame_head.stream_id;
         return MakeH2Message(nullptr);
@@ -918,25 +1001,25 @@ H2ParseResult H2StreamContext::OnResetStream(
 #if defined(BRPC_H2_STREAM_STATE)
     if (state() == H2_STREAM_OPEN) {
         SetState(H2_STREAM_HALF_CLOSED_REMOTE);
-    } else if (state() == H2_STREAM_HALF_CLOSED_LOCAL) {
+    } else if (state() == H2_STREAM_HALF_CLOSED_LOCAL ||
+               state() == H2_STREAM_HALF_CLOSED_REMOTE) {
         SetState(H2_STREAM_CLOSED);
     } else {
         LOG(ERROR) << "Invalid state=" << H2StreamState2Str(_state)
                    << " in stream_id=" << stream_id();
+        RemoveOneRefForStage2();
         return MakeH2Error(H2_PROTOCOL_ERROR);
     }
 #endif
-    H2StreamContext* sctx = _conn_ctx->RemoveStreamAndDeferWU(stream_id());
-    if (sctx == nullptr) {
-        LOG(ERROR) << "Fail to find stream_id=" << stream_id();
-        return MakeH2Error(H2_PROTOCOL_ERROR);
-    }
+    get_h2_bvars()->h2_stream_reset_count << 1;
     if (_conn_ctx->is_client_side()) {
-        sctx->header().set_status_code(H2ErrorToStatusCode(h2_error));
-        return MakeH2Message(sctx);
+        _rst_received = true;
+        _rst_error = h2_error;
+        header().set_status_code(H2ErrorToStatusCode(h2_error));
+        return MakeH2Message(this);
     } else {
-        // No need to process the request.
-        delete sctx;
+        // No need to process the request. Release _pending_streams's ref.
+        RemoveOneRefForStage2();
         return MakeH2Message(nullptr);
     }
 }
@@ -953,15 +1036,31 @@ H2ParseResult H2StreamContext::OnEndStream() {
         return MakeH2Error(H2_PROTOCOL_ERROR);
     }
 #endif
-    H2StreamContext* sctx = _conn_ctx->RemoveStreamAndDeferWU(stream_id());
-    if (sctx == nullptr) {
-        RPC_VLOG << "Fail to find stream_id=" << stream_id();
-        return MakeH2Message(nullptr);
-    }
-    CHECK_EQ(sctx, this);
+    if (_conn_ctx->is_client_side()) {
+        H2StreamContext* sctx = _conn_ctx->RemoveStreamAndDeferWU(stream_id());
+        if (sctx == nullptr) {
+            RPC_VLOG << "Fail to find stream_id=" << stream_id();
+            return MakeH2Message(nullptr);
+        }
+        CHECK_EQ(sctx, this);
 
-    OnMessageComplete();
-    return MakeH2Message(sctx);
+        OnMessageComplete();
+        return MakeH2Message(sctx);
+    } else {
+        if (_remote_ended) {
+            LOG(ERROR) << "Stream already ended remotely in stream_id=" << 
stream_id();
+            return MakeH2Error(H2_PROTOCOL_ERROR);
+        }
+        _remote_ended = true;
+        _conn_ctx->DeferWindowUpdate(ReleaseDeferredWindowUpdate());
+        // Retain sctx in _pending_streams until the response is sent so that
+        // stream-level flow control and WINDOW_UPDATE work for server 
responses.
+        // AddOneRefForStage2() adds the reference consumed by 
ProcessHttpRequest
+        // (used purely for refcounting; _is_stage2 is only read by HTTP/1.x).
+        AddOneRefForStage2();
+        OnMessageComplete();
+        return MakeH2Message(this);
+    }
 }
 
 H2ParseResult H2Context::OnSettings(
@@ -1148,7 +1247,7 @@ H2ParseResult H2Context::OnWindowUpdate(
                 LOG(ERROR) << "Invalid stream-level window_size_increment=" << 
inc
                     << " to remote_window_left="
                     << 
(*psctx)->_remote_window_left.load(butil::memory_order_relaxed);
-                return MakeH2Error(H2_FLOW_CONTROL_ERROR);
+                return MakeH2Error(H2_FLOW_CONTROL_ERROR, 
frame_head.stream_id);
             }
         }
         if (!FlushPendingData(frame_head.stream_id)) {
@@ -1161,21 +1260,23 @@ H2ParseResult H2Context::OnWindowUpdate(
 void H2Context::Describe(std::ostream& os, const DescribeOptions& opt) const {
     H2Settings remote_settings;
     bool remote_settings_received = false;
+    int last_received_stream_id = -1;
     size_t pending_stream_size = 0;
     size_t pending_data_size = 0;
     {
         std::unique_lock<butil::Mutex> mu(_stream_mutex);
         remote_settings = _remote_settings;
         remote_settings_received = _remote_settings_received;
+        last_received_stream_id = _last_received_stream_id;
         pending_stream_size = _pending_streams.size();
-        pending_data_size = _pending_data_size;
+        pending_data_size = 
_pending_data_size.load(butil::memory_order_relaxed);
     }
     if (opt.verbose) {
         os << '\n';
     }
     const char sep = (opt.verbose ? '\n' : ' ');
     os << "conn_state=" << H2ConnectionState2Str(_conn_state);
-    os << sep << "last_received_stream_id=" << _last_received_stream_id
+    os << sep << "last_received_stream_id=" << last_received_stream_id
        << sep << "last_sent_stream_id=" << _last_sent_stream_id;
     os << sep << "deferred_window_update="
        << _deferred_window_update.load(butil::memory_order_relaxed)
@@ -1287,7 +1388,7 @@ inline void H2Context::ClearAbandonedStreams() {
         mu.unlock();
         H2StreamContext* sctx = RemoveStreamAndDeferWU(stream_id);
         if (sctx != nullptr) {
-            delete sctx;
+            sctx->RemoveOneRefForStage2();
         }
         mu.lock();
     }
@@ -1301,15 +1402,46 @@ H2StreamContext::H2StreamContext(bool 
read_body_progressively)
 #endif
     , _stream_id(0)
     , _stream_ended(false)
+    , _remote_ended(false)
+    , _response_committed(false)
+    , _rst_received(false)
+    , _rst_error(H2_NO_ERROR)
     , _remote_window_left(0)
     , _deferred_window_update(0)
     , _correlation_id(INVALID_BTHREAD_ID.value)
     , _decoded_header_list_size(0)
-    , _rejected_error(H2_NO_ERROR) {
+    , _rejected_error(H2_NO_ERROR)
+    , _deferred_response_id(INVALID_BTHREAD_ID.value) {
     header().set_version(2, 0);
-#ifndef NDEBUG
     get_h2_bvars()->h2_stream_context_count << 1;
-#endif
+}
+
+int H2StreamContext::OnSocketFailed(bthread_id_t id, void* data, int 
error_code) {
+    butil::intrusive_ptr<H2StreamContext> sctx(
+        static_cast<H2StreamContext*>(data), false);
+    sctx->_deferred_response_id.store(
+        INVALID_BTHREAD_ID.value, butil::memory_order_release);
+    SocketUniquePtr sock(std::move(sctx->_deferred_response_sock));
+    if (sock != NULL) {
+        sock->ReleaseIdleFailRef();
+    }
+    int rc = 0;
+    if (error_code != 0 && sctx->_conn_ctx != NULL) {
+        H2StreamContext* removed =
+            sctx->_conn_ctx->RemoveStreamAndDeferWU(sctx->stream_id());
+        if (removed != NULL) {
+            if (sock != NULL && !sock->Failed()) {
+                // Socket::NotifyOnFailed() failed to register `id' in the
+                // notification list (e.g. EAGAIN) while the Socket itself is
+                // still healthy. Return `error_code' so AppendAndDestroySelf()
+                // can explicitly reset the stream via RST_STREAM.
+                rc = error_code;
+            }
+            removed->RemoveOneRefForStage2();
+        }
+    }
+    bthread_id_unlock_and_destroy(id);
+    return rc;
 }
 
 void H2StreamContext::Init(H2Context* conn_ctx, int stream_id) {
@@ -1320,9 +1452,7 @@ void H2StreamContext::Init(H2Context* conn_ctx, int 
stream_id) {
 }
 
 H2StreamContext::~H2StreamContext() {
-#ifndef NDEBUG
     get_h2_bvars()->h2_stream_context_count << -1;
-#endif
 }
 
 #if defined(BRPC_H2_STREAM_STATE)
@@ -1509,51 +1639,6 @@ static void PackH2Headers(butil::IOBuf* out,
     }
 }
 
-static void PackH2Message(butil::IOBuf* out,
-                          butil::IOBuf& headers,
-                          butil::IOBuf& trailer_headers,
-                          const butil::IOBuf& data,
-                          int stream_id,
-                          H2Context* conn_ctx) {
-    const H2Settings& remote_settings = conn_ctx->remote_settings();
-    char headbuf[FRAME_HEAD_SIZE];
-    PackH2Headers(out, headers, stream_id, remote_settings.max_frame_size,
-                  data.empty() && trailer_headers.empty());
-    if (!data.empty()) {
-        H2FrameHead data_head = {0, H2_FRAME_DATA, 0, stream_id};
-        butil::IOBufBytesIterator it(data);
-        while (it.bytes_left()) {
-            if (it.bytes_left() <= remote_settings.max_frame_size) {
-                data_head.payload_size = it.bytes_left();
-                if (trailer_headers.empty()) {
-                    data_head.flags |= H2_FLAGS_END_STREAM;
-                }
-            } else {
-                data_head.payload_size = remote_settings.max_frame_size;
-            }
-            SerializeFrameHead(headbuf, data_head);
-            out->append(headbuf, FRAME_HEAD_SIZE);
-            it.append_and_forward(out, data_head.payload_size);
-        }
-    }
-    if (!trailer_headers.empty()) {
-        H2FrameHead headers_head = {
-            (uint32_t)trailer_headers.size(), H2_FRAME_HEADERS, 0, stream_id};
-        headers_head.flags |= H2_FLAGS_END_STREAM;
-        headers_head.flags |= H2_FLAGS_END_HEADERS;
-        SerializeFrameHead(headbuf, headers_head);
-        out->append(headbuf, sizeof(headbuf));
-        out->append(butil::IOBuf::Movable(trailer_headers));
-    }
-    const int64_t conn_wu = conn_ctx->ReleaseDeferredWindowUpdate();
-    if (conn_wu > 0) {
-        char winbuf[FRAME_HEAD_SIZE + 4];
-        SerializeFrameHead(winbuf, 4, H2_FRAME_WINDOW_UPDATE, 0, 0);
-        SaveUint32(winbuf + FRAME_HEAD_SIZE, conn_wu);
-        out->append(winbuf, sizeof(winbuf));
-    }
-}
-
 void H2Context::AppendPendingDataLocked(H2StreamContext* sctx,
                                         butil::IOBuf* out) {
     CHECK(sctx != nullptr);
@@ -1579,14 +1664,19 @@ void 
H2Context::AppendPendingDataLocked(H2StreamContext* sctx,
         H2FrameHead data_head = {
             static_cast<uint32_t>(payload_size), H2_FRAME_DATA, 0,
             sctx->stream_id()};
-        if (payload_size == sctx->_pending_data.size()) {
+        if (payload_size == sctx->_pending_data.size() &&
+            sctx->_pending_trailer.empty()) {
             data_head.flags |= H2_FLAGS_END_STREAM;
         }
         SerializeFrameHead(headbuf, data_head);
         out->append(headbuf, sizeof(headbuf));
         sctx->_pending_data.cutn(out, payload_size);
-        CHECK_GE(_pending_data_size, payload_size);
-        _pending_data_size -= payload_size;
+        SubPendingDataSize(payload_size);
+    }
+    if (sctx->_pending_data.empty() && !sctx->_pending_trailer.empty()) {
+        PackH2Headers(out, sctx->_pending_trailer, sctx->stream_id(),
+                      max_frame_size, true /* end_stream */);
+        sctx->_pending_trailer.clear();
     }
 }
 
@@ -1594,6 +1684,12 @@ butil::Status H2Context::TryToInsertClientStream(
     int stream_id, H2StreamContext* sctx, const butil::IOBuf& data,
     butil::IOBuf* out) {
     std::unique_lock<butil::Mutex> mu(_stream_mutex);
+    return TryToInsertClientStreamLocked(stream_id, sctx, data, out);
+}
+
+butil::Status H2Context::TryToInsertClientStreamLocked(
+    int stream_id, H2StreamContext* sctx, const butil::IOBuf& data,
+    butil::IOBuf* out) {
     if (_goaway_stream_id >= 0 && stream_id > _goaway_stream_id) {
         return butil::Status(ELOGOFF, "the connection just issued GOAWAY");
     }
@@ -1606,12 +1702,9 @@ butil::Status H2Context::TryToInsertClientStream(
             ELIMIT, "Pending Stream count exceeds max concurrent stream");
     }
 
-    sctx->_remote_window_left.store(_remote_settings.stream_window_size,
-                                    butil::memory_order_relaxed);
     const int64_t conn_window =
         _remote_window_left.load(butil::memory_order_relaxed);
-    const int64_t stream_window =
-        sctx->_remote_window_left.load(butil::memory_order_relaxed);
+    const int64_t stream_window = _remote_settings.stream_window_size;
     size_t sendable_size = 0;
     if (conn_window > 0 && stream_window > 0) {
         sendable_size = std::min(
@@ -1619,68 +1712,133 @@ butil::Status H2Context::TryToInsertClientStream(
             static_cast<size_t>(std::min(conn_window, stream_window)));
     }
     const size_t pending_size = data.size() - sendable_size;
-    if (FLAGS_socket_max_unwritten_bytes > 0) {
-        const auto limit =
-            static_cast<size_t>(FLAGS_socket_max_unwritten_bytes);
+    const int64_t max_pending = GetH2MaxPendingDataBytes();
+    if (max_pending > 0) {
+        const auto limit = static_cast<size_t>(max_pending);
+        const size_t cur_pending =
+            _pending_data_size.load(butil::memory_order_relaxed);
         // Check and reserve pending bytes under the same lock. Otherwise,
         // concurrent requests may all observe available capacity before any
         // of them adds its unsent DATA.
-        if (_pending_data_size > limit ||
-            pending_size > limit - _pending_data_size) {
+        if (cur_pending > limit || pending_size > limit - cur_pending) {
             return butil::Status(EOVERCROWDED,
                                  "Too much pending HTTP/2 request data");
         }
     }
 
     // Mutate stream and window state only after all failure checks above.
+    sctx->_conn_ctx = this;
+    sctx->_stream_id = stream_id;
+    sctx->_remote_window_left.store(stream_window,
+                                    butil::memory_order_relaxed);
     _pending_streams[stream_id] = sctx;
+    _volatile_pending_stream_size.store(
+        _pending_streams.size(), butil::memory_order_relaxed);
     if (!data.empty()) {
         CHECK(sctx->_pending_data.empty());
         sctx->_pending_data = data;
-        _pending_data_size += data.size();
+        AddPendingDataSize(data.size());
         AppendPendingDataLocked(sctx, out);
     }
     return butil::Status::OK();
 }
 
-void H2Context::ClearPendingData(int stream_id) {
+bool H2Context::ClearPendingData(int stream_id) {
     std::unique_lock<butil::Mutex> mu(_stream_mutex);
     H2StreamContext** psctx = _pending_streams.seek(stream_id);
     if (psctx == nullptr) {
-        return;
+        return false;
     }
-    CHECK_GE(_pending_data_size, (*psctx)->_pending_data.size());
-    _pending_data_size -= (*psctx)->_pending_data.size();
+    const bool had_pending = !(*psctx)->_pending_data.empty() ||
+                             !(*psctx)->_pending_trailer.empty();
+    SubPendingDataSize((*psctx)->_pending_data.size());
     (*psctx)->_pending_data.clear();
+    (*psctx)->_pending_trailer.clear();
+    return had_pending;
 }
 
 bool H2Context::PendingDataOvercrowded() const {
-    std::unique_lock<butil::Mutex> mu(_stream_mutex);
-    return FLAGS_socket_max_unwritten_bytes > 0 &&
-           _pending_data_size >=
-               static_cast<size_t>(FLAGS_socket_max_unwritten_bytes);
+    const int64_t max_pending = GetH2MaxPendingDataBytes();
+    return max_pending > 0 &&
+           _pending_data_size.load(butil::memory_order_relaxed) >=
+               static_cast<size_t>(max_pending);
 }
 
 bool H2Context::FlushPendingData(int stream_id) {
     butil::IOBuf out;
+    std::vector<H2StreamContext*> finished_streams;
     {
         std::unique_lock<butil::Mutex> mu(_stream_mutex);
         if (stream_id != 0) {
             H2StreamContext** psctx = _pending_streams.seek(stream_id);
             if (psctx != nullptr) {
-                AppendPendingDataLocked(*psctx, &out);
+                H2StreamContext* sctx = *psctx;
+                if (sctx->_rejected_error == H2_NO_ERROR &&
+                    (!sctx->_pending_data.empty() ||
+                     !sctx->_pending_trailer.empty())) {
+                    AppendPendingDataLocked(sctx, &out);
+                    if (is_server_side() && sctx->_response_committed &&
+                        sctx->_pending_data.empty() &&
+                        sctx->_pending_trailer.empty()) {
+                        RemoveStreamLocked(stream_id);
+                        finished_streams.push_back(sctx);
+                    }
+                }
             }
         } else {
+            // Round-Robin at connection-level WINDOW_UPDATE granularity:
+            // rotate the starting stream after _last_flushed_stream_id so a
+            // lower-ID stream cannot permanently starve higher-ID streams
+            // across consecutive WINDOW_UPDATE frames.
+            std::vector<H2StreamContext*> pending_list;
             for (StreamMap::const_iterator it = _pending_streams.begin();
                  it != _pending_streams.end(); ++it) {
+                H2StreamContext* sctx = it->second;
+                if (sctx->_rejected_error == H2_NO_ERROR &&
+                    (!sctx->_pending_data.empty() ||
+                     !sctx->_pending_trailer.empty())) {
+                    pending_list.push_back(sctx);
+                }
+            }
+            if (pending_list.size() > 1) {
+                std::sort(pending_list.begin(), pending_list.end(),
+                          [](const H2StreamContext* a, const H2StreamContext* 
b) {
+                              return a->stream_id() < b->stream_id();
+                          });
+            }
+            size_t start_idx = 0;
+            for (size_t i = 0; i < pending_list.size(); ++i) {
+                if (pending_list[i]->stream_id() > _last_flushed_stream_id) {
+                    start_idx = i;
+                    break;
+                }
+            }
+            for (size_t i = 0; i < pending_list.size(); ++i) {
                 if (_remote_window_left.load(butil::memory_order_relaxed) <= 
0) {
                     break;
                 }
-                AppendPendingDataLocked(it->second, &out);
+                H2StreamContext* sctx =
+                    pending_list[(start_idx + i) % pending_list.size()];
+                const size_t before_size = sctx->_pending_data.size();
+                AppendPendingDataLocked(sctx, &out);
+                if (sctx->_pending_data.size() < before_size) {
+                    _last_flushed_stream_id = sctx->stream_id();
+                }
+                if (is_server_side() && sctx->_response_committed &&
+                    sctx->_pending_data.empty() &&
+                    sctx->_pending_trailer.empty()) {
+                    RemoveStreamLocked(sctx->stream_id());
+                    finished_streams.push_back(sctx);
+                }
             }
         }
     }
-    return out.empty() || WriteAck(_socket, &out) == 0;
+    const bool rc = out.empty() || WriteAck(_socket, &out) == 0;
+    for (size_t i = 0; i < finished_streams.size(); ++i) {
+        DeferWindowUpdate(finished_streams[i]->ReleaseDeferredWindowUpdate());
+        finished_streams[i]->RemoveOneRefForStage2();
+    }
+    return rc;
 }
 
 H2UnsentRequest::H2UnsentRequest(Controller* c)
@@ -1688,15 +1846,11 @@ H2UnsentRequest::H2UnsentRequest(Controller* c)
     , _size(0)
     , _stream_id(0)
     , _cntl(c) {
-#ifndef NDEBUG
     get_h2_bvars()->h2_unsent_request_count << 1;
-#endif
 }
 
 H2UnsentRequest::~H2UnsentRequest() {
-#ifndef NDEBUG
     get_h2_bvars()->h2_unsent_request_count << -1;
-#endif
 }
 
 H2UnsentRequest* H2UnsentRequest::New(Controller* c) {
@@ -1802,8 +1956,21 @@ void 
H2UnsentRequest::DestroyStreamUserData(SocketUniquePtr& sending_sock,
         _cntl = nullptr;
         if (_stream_id != 0) {
             H2Context* ctx = 
static_cast<H2Context*>(sending_sock->parsing_context());
-            ctx->ClearPendingData(_stream_id);
-            ctx->AddAbandonedStream(_stream_id);
+            H2StreamContext* sctx = ctx->RemoveStreamAndDeferWU(_stream_id);
+            if (sctx != NULL) {
+                // The stream is still active locally (neither completed via
+                // OnEndStream nor reset by the peer). Notify the peer with
+                // RST_STREAM(CANCEL) so it can release any retained stream
+                // or pending response DATA.
+                sctx->RemoveOneRefForStage2();
+                char rstbuf[FRAME_HEAD_SIZE + 4];
+                SerializeFrameHead(rstbuf, 4, H2_FRAME_RST_STREAM, 0, 
_stream_id);
+                SaveUint32(rstbuf + FRAME_HEAD_SIZE, H2_CANCEL);
+                get_h2_bvars()->h2_stream_reset_count << 1;
+                if (WriteAck(sending_sock.get(), rstbuf, sizeof(rstbuf)) != 0) 
{
+                    LOG(WARNING) << "Fail to send RST_STREAM to " << 
*sending_sock;
+                }
+            }
         }
     }
 }
@@ -1823,9 +1990,14 @@ H2UnsentRequest::AppendAndDestroySelf(butil::IOBuf* out, 
Socket* socket) {
     if (socket == nullptr) {
         return butil::Status::OK();
     }
-    H2Context* ctx = static_cast<H2Context*>(socket->parsing_context());
+    // Although the critical section looks huge, it should rarely be contended
+    // since timeout of RPC is much larger than the delay of sending.
+    std::unique_lock<butil::Mutex> mu(_mutex);
+    if (_cntl == nullptr) {
+        return butil::Status(ECANCELED, "The RPC was already failed");
+    }
 
-    // Create a http2 stream and store correlation_id in.
+    H2Context* ctx = static_cast<H2Context*>(socket->parsing_context());
     if (ctx == nullptr) {
         CHECK(socket->CreatedByConnect());
         ctx = new H2Context(socket, nullptr);
@@ -1833,80 +2005,72 @@ H2UnsentRequest::AppendAndDestroySelf(butil::IOBuf* 
out, Socket* socket) {
             delete ctx;
             return butil::Status(EINTERNAL, "Fail to init H2Context");
         }
+        ctx->_client_preface_pending = true;
         socket->initialize_parsing_context(&ctx);
-        
-        // Append client connection preface
-        out->append(H2_CONNECTION_PREFACE_PREFIX,
-                    H2_CONNECTION_PREFACE_PREFIX_SIZE);
-        
-        char settingsbuf[FRAME_HEAD_SIZE + H2_SETTINGS_MAX_BYTE_SIZE +
-                         FRAME_HEAD_SIZE + 4/*for WU*/];
-        const size_t nb = SerializeH2SettingsFrameAndWU(
-            ctx->_unack_local_settings, settingsbuf);
-        out->append(settingsbuf, nb);
     }
 
-    // Although the critical section looks huge, it should rarely be contended
-    // since timeout of RPC is much larger than the delay of sending.
-    std::unique_lock<butil::Mutex> mu(_mutex);
-    if (_cntl == nullptr) {
-        return butil::Status(ECANCELED, "The RPC was already failed");
-    }
-
-    const int id = ctx->AllocateClientStreamId();
-    if (id < 0) {
-        // The RPC should be failed and retried.
-        // Note that the socket should not be SetFailed() which may affect
-        // other RPC successfully sent requests and waiting for responses.
-        RPC_VLOG << "Fail to allocate stream_id on " << *socket
-                 << " h2req=" << (StreamUserData*)this;
-        return butil::Status(EH2RUNOUTSTREAMS, "Fail to allocate stream_id");
-    }
-
-    _sctx->Init(ctx, id);
-    // Insert the stream before encoding the headers and do not fail after
-    // encoding: HPACK encoding adds the headers to the dynamic table of the
-    // connection, and if the request were refused afterwards, the caller
-    // would drop the HEADERS while the table kept the entries, so the
-    // server would decode later requests on this connection with wrong
-    // headers. The DATA that the windows allow now follows the HEADERS.
     const butil::IOBuf& request_data = _cntl->request_attachment();
-    butil::IOBuf data_frames;
-    const butil::Status insert_status = ctx->TryToInsertClientStream(
-        id, _sctx.get(), request_data, &data_frames);
-    if (!insert_status.ok()) {
-        return insert_status;
-    }
-    _stream_id = id;
-    // TryToInsertClientStream transfers ownership of _sctx to ctx on success.
-    _sctx.release();
-
-    HPacker& hpacker = ctx->hpacker();
-    butil::IOBufAppender appender;
-    HPackOptions options;
-    options.encode_name = FLAGS_h2_hpack_encode_name;
-    options.encode_value = FLAGS_h2_hpack_encode_value;
-    const H2Settings remote_settings = ctx->remote_settings();
-    if (remote_settings.header_table_size == 0) {
-        options.index_policy = HPACK_NEVER_INDEX_HEADER;
-    }
-    
-    for (size_t i = 0; i < _size; ++i) {
-        hpacker.Encode(&appender, _list[i], options);
-    }
-    if (_cntl->has_http_request()) {
-        const HttpHeader& h = _cntl->http_request();
-        for (HttpHeader::HeaderIterator it = h.HeaderBegin();
-             it != h.HeaderEnd(); ++it) {
-            HPacker::Header header(it->first, it->second);
-            hpacker.Encode(&appender, header, options);
+    {
+        std::unique_lock<butil::Mutex> stream_mu(ctx->_stream_mutex);
+        if (ctx->RunOutStreams()) {
+            // The RPC should be failed and retried.
+            // Note that the socket should not be SetFailed() which may affect
+            // other RPC successfully sent requests and waiting for responses.
+            LOG(WARNING) << "Fail to allocate new client stream, 
_last_sent_stream_id="
+                         << ctx->_last_sent_stream_id;
+            RPC_VLOG << "Fail to allocate stream_id on " << *socket
+                     << " h2req=" << (StreamUserData*)this;
+            return butil::Status(EH2RUNOUTSTREAMS, "Fail to allocate 
stream_id");
+        }
+        const int id = static_cast<int>(ctx->_last_sent_stream_id);
+        butil::IOBuf data_out;
+        const butil::Status st = ctx->TryToInsertClientStreamLocked(
+            id, _sctx.get(), request_data, &data_out);
+        if (!st.ok()) {
+            return st;
+        }
+
+        // Mutate connection state and HPACK dynamic table only after
+        // TryToInsertClientStreamLocked succeeds.
+        ctx->_last_sent_stream_id += 2;
+        if (ctx->_client_preface_pending) {
+            ctx->_client_preface_pending = false;
+            out->append(H2_CONNECTION_PREFACE_PREFIX,
+                        H2_CONNECTION_PREFACE_PREFIX_SIZE);
+            char settingsbuf[FRAME_HEAD_SIZE + H2_SETTINGS_MAX_BYTE_SIZE +
+                             FRAME_HEAD_SIZE + 4/*for WU*/];
+            const size_t nb = SerializeH2SettingsFrameAndWU(
+                ctx->_unack_local_settings, settingsbuf);
+            out->append(settingsbuf, nb);
+        }
+
+        HPacker& hpacker = ctx->hpacker();
+        butil::IOBufAppender appender;
+        HPackOptions options;
+        options.encode_name = FLAGS_h2_hpack_encode_name;
+        options.encode_value = FLAGS_h2_hpack_encode_value;
+        if (ctx->_remote_settings.header_table_size == 0) {
+            options.index_policy = HPACK_NEVER_INDEX_HEADER;
+        }
+        for (size_t i = 0; i < _size; ++i) {
+            hpacker.Encode(&appender, _list[i], options);
+        }
+        if (_cntl->has_http_request()) {
+            const HttpHeader& h = _cntl->http_request();
+            for (HttpHeader::HeaderIterator it = h.HeaderBegin();
+                 it != h.HeaderEnd(); ++it) {
+                HPacker::Header header(it->first, it->second);
+                hpacker.Encode(&appender, header, options);
+            }
         }
+        butil::IOBuf frag;
+        appender.move_to(frag);
+        PackH2Headers(out, frag, id, ctx->_remote_settings.max_frame_size,
+                      request_data.empty());
+        out->append(butil::IOBuf::Movable(data_out));
+        _stream_id = id;
+        _sctx.release();
     }
-    butil::IOBuf frag;
-    appender.move_to(frag);
-    PackH2Headers(out, frag, id, remote_settings.max_frame_size,
-                  request_data.empty());
-    out->append(butil::IOBuf::Movable(data_frames));
     const int64_t conn_wu = ctx->ReleaseDeferredWindowUpdate();
     if (conn_wu > 0) {
         char winbuf[FRAME_HEAD_SIZE + 4];
@@ -2019,57 +2183,152 @@ H2UnsentResponse::AppendAndDestroySelf(butil::IOBuf* 
out, Socket* socket) {
         return butil::Status::OK();
     }
     H2Context* ctx = static_cast<H2Context*>(socket->parsing_context());
+    if (ctx == NULL || ctx->is_client_side() || _stream_id == 0) {
+        return butil::Status::OK();
+    }
+
+    H2StreamContext* overcrowded_sctx = nullptr;
+    H2StreamContext* finished_sctx = nullptr;
+    H2StreamContext* notify_failed_sctx = nullptr;
+    bthread_id_t deferred_notify_id = INVALID_BTHREAD_ID;
+    int notify_reg_error = 0;
+    {
+        std::unique_lock<butil::Mutex> mu(ctx->_stream_mutex);
+        H2StreamContext** psctx = ctx->_pending_streams.seek(_stream_id);
+        if (psctx == nullptr || (*psctx)->_rejected_error != H2_NO_ERROR) {
+            // Stream was already reset by client, abandoned, or rejected.
+            return butil::Status::OK();
+        }
+        H2StreamContext* sctx = *psctx;
+        const int64_t conn_window =
+            ctx->_remote_window_left.load(butil::memory_order_relaxed);
+        const int64_t stream_window =
+            sctx->_remote_window_left.load(butil::memory_order_relaxed);
+        size_t sendable_size = 0;
+        if (conn_window > 0 && stream_window > 0) {
+            sendable_size = std::min(
+                _data.size(),
+                static_cast<size_t>(std::min(conn_window, stream_window)));
+        }
+        const size_t pending_size = _data.size() - sendable_size;
+        const int64_t max_pending = GetH2MaxPendingDataBytes();
+        if (max_pending > 0 && pending_size > 0) {
+            const auto limit = static_cast<size_t>(max_pending);
+            const size_t cur_pending =
+                ctx->_pending_data_size.load(butil::memory_order_relaxed);
+            if (cur_pending > limit || pending_size > limit - cur_pending) {
+                overcrowded_sctx = ctx->RemoveStreamLocked(_stream_id);
+            }
+        }
+        if (overcrowded_sctx == nullptr) {
+            HPacker& hpacker = ctx->hpacker();
+            butil::IOBufAppender appender;
+            HPackOptions options;
+            options.encode_name = FLAGS_h2_hpack_encode_name;
+            options.encode_value = FLAGS_h2_hpack_encode_value;
+            if (ctx->_remote_settings.header_table_size == 0) {
+                options.index_policy = HPACK_NEVER_INDEX_HEADER;
+            }
+            for (size_t i = 0; i < _size; ++i) {
+                hpacker.Encode(&appender, _list[i], options);
+            }
+            if (_http_response) {
+                for (HttpHeader::HeaderIterator it = 
_http_response->HeaderBegin();
+                     it != _http_response->HeaderEnd(); ++it) {
+                    HPacker::Header header(it->first, it->second);
+                    hpacker.Encode(&appender, header, options);
+                }
+            }
+            butil::IOBuf frag;
+            appender.move_to(frag);
+            PackH2Headers(out, frag, _stream_id,
+                          ctx->_remote_settings.max_frame_size,
+                          _data.empty() && !_is_grpc);
+
+            if (_is_grpc) {
+                HPackOptions trailer_options = options;
+                trailer_options.index_policy = HPACK_NOT_INDEX_HEADER;
+                HPacker::Header status_header(
+                    "grpc-status", butil::string_printf("%d", _grpc_status));
+                HPacker::EncodeWithoutDynamicTable(
+                    &appender, status_header, trailer_options);
+                if (!_grpc_message.empty()) {
+                    HPacker::Header msg_header("grpc-message", _grpc_message);
+                    HPacker::EncodeWithoutDynamicTable(
+                        &appender, msg_header, trailer_options);
+                }
+                appender.move_to(sctx->_pending_trailer);
+            }
 
-    // flow control
-    // NOTE: Currently the stream context is definitely removed and updating
-    // window size is useless, however it's not true when progressive request
-    // is supported.
-    // TODO(zhujiashun): Instead of just returning error to client, a better
-    // solution to handle not enough window size is to wait until WINDOW_UPDATE
-    // is received, and then retry those failed response again.
-    if (!MinusWindowSize(&ctx->_remote_window_left, _data.size())) {
+            sctx->_response_committed = true;
+            if (!_data.empty()) {
+                CHECK(sctx->_pending_data.empty());
+                sctx->_pending_data = _data;
+                ctx->AddPendingDataSize(_data.size());
+            }
+            ctx->AppendPendingDataLocked(sctx, out);
+            if (sctx->_pending_data.empty() && sctx->_pending_trailer.empty()) 
{
+                finished_sctx = ctx->RemoveStreamLocked(_stream_id);
+            } else {
+                const int rc = bthread_id_create(
+                    &deferred_notify_id, sctx, 
H2StreamContext::OnSocketFailed);
+                if (rc == 0) {
+                    CHECK_EQ(0, bthread_id_trylock(deferred_notify_id, NULL));
+                    // Keep the Socket alive until the deferred response 
finishes
+                    // draining (or the stream/connection terminates) so 
Server::Stop
+                    // does not reclaim the Socket while waiting for 
WINDOW_UPDATE.
+                    sctx->HoldDeferredResponseSocket(socket, 
deferred_notify_id);
+                } else {
+                    notify_reg_error = rc;
+                    notify_failed_sctx = ctx->RemoveStreamLocked(_stream_id);
+                }
+            }
+        }
+    }
+    if (deferred_notify_id != INVALID_BTHREAD_ID) {
+        socket->NotifyOnFailed(deferred_notify_id);
+        notify_reg_error = bthread_id_unlock(deferred_notify_id);
+    }
+    if (notify_failed_sctx != nullptr) {
+        
ctx->DeferWindowUpdate(notify_failed_sctx->ReleaseDeferredWindowUpdate());
+        notify_failed_sctx->RemoveOneRefForStage2();
+    }
+    if (notify_reg_error != 0) {
+        LOG_EVERY_SECOND(WARNING)
+            << "Fail to register deferred HTTP/2 response notification on "
+            << *socket << ": " << berror(notify_reg_error)
+            << ", resetting stream_id=" << _stream_id;
+        char rstbuf[FRAME_HEAD_SIZE + 4];
+        SerializeFrameHead(rstbuf, 4, H2_FRAME_RST_STREAM, 0, _stream_id);
+        SaveUint32(rstbuf + FRAME_HEAD_SIZE, H2_INTERNAL_ERROR);
+        out->append(rstbuf, sizeof(rstbuf));
+        get_h2_bvars()->h2_stream_reset_count << 1;
+    }
+    if (overcrowded_sctx != nullptr) {
+        
ctx->DeferWindowUpdate(overcrowded_sctx->ReleaseDeferredWindowUpdate());
+        overcrowded_sctx->RemoveOneRefForStage2();
+        LOG_EVERY_SECOND(WARNING)
+            << "Pending HTTP/2 response data on " << *socket
+            << " exceeds limit=" << GetH2MaxPendingDataBytes()
+            << ", resetting stream_id=" << _stream_id;
         char rstbuf[FRAME_HEAD_SIZE + 4];
         SerializeFrameHead(rstbuf, 4, H2_FRAME_RST_STREAM, 0, _stream_id);
-        SaveUint32(rstbuf + FRAME_HEAD_SIZE, H2_FLOW_CONTROL_ERROR);
+        SaveUint32(rstbuf + FRAME_HEAD_SIZE, H2_ENHANCE_YOUR_CALM);
         out->append(rstbuf, sizeof(rstbuf));
+        get_h2_bvars()->h2_stream_reset_count << 1;
         return butil::Status::OK();
     }
-
-    HPacker& hpacker = ctx->hpacker();
-    butil::IOBufAppender appender;
-    HPackOptions options;
-    options.encode_name = FLAGS_h2_hpack_encode_name;
-    options.encode_value = FLAGS_h2_hpack_encode_value;
-    if (ctx->remote_settings().header_table_size == 0) {
-        options.index_policy = HPACK_NEVER_INDEX_HEADER;
-    }
-
-    for (size_t i = 0; i < _size; ++i) {
-        hpacker.Encode(&appender, _list[i], options);
+    if (finished_sctx != nullptr) {
+        ctx->DeferWindowUpdate(finished_sctx->ReleaseDeferredWindowUpdate());
+        finished_sctx->RemoveOneRefForStage2();
     }
-    if (_http_response) {
-        for (HttpHeader::HeaderIterator it = _http_response->HeaderBegin();
-             it != _http_response->HeaderEnd(); ++it) {
-            HPacker::Header header(it->first, it->second);
-            hpacker.Encode(&appender, header, options);
-        }
-    }
-    butil::IOBuf frag;
-    appender.move_to(frag);
-
-    butil::IOBuf trailer_frag;
-    if (_is_grpc) {
-        HPacker::Header status_header("grpc-status",
-                                      butil::string_printf("%d", 
_grpc_status));
-        hpacker.Encode(&appender, status_header, options);
-        if (!_grpc_message.empty()) {
-            HPacker::Header msg_header("grpc-message", _grpc_message);
-            hpacker.Encode(&appender, msg_header, options);
-        }
-        appender.move_to(trailer_frag);
+    const int64_t conn_wu = ctx->ReleaseDeferredWindowUpdate();
+    if (conn_wu > 0) {
+        char winbuf[FRAME_HEAD_SIZE + 4];
+        SerializeFrameHead(winbuf, 4, H2_FRAME_WINDOW_UPDATE, 0, 0);
+        SaveUint32(winbuf + FRAME_HEAD_SIZE, conn_wu);
+        out->append(winbuf, sizeof(winbuf));
     }
-
-    PackH2Message(out, frag, trailer_frag, _data, _stream_id, ctx);
     return butil::Status::OK();
 }
 
diff --git a/src/brpc/policy/http2_rpc_protocol.h 
b/src/brpc/policy/http2_rpc_protocol.h
index b82dd93f5..6c0ed8fe4 100644
--- a/src/brpc/policy/http2_rpc_protocol.h
+++ b/src/brpc/policy/http2_rpc_protocol.h
@@ -25,10 +25,7 @@
 #include "brpc/details/hpack.h"
 #include "brpc/stream_creator.h"
 #include "brpc/controller.h"
-
-#ifndef NDEBUG
 #include "bvar/bvar.h"
-#endif
 
 namespace brpc {
 namespace policy {
@@ -115,20 +112,22 @@ enum H2StreamState {
 };
 const char* H2StreamState2Str(H2StreamState);
 
-#ifndef NDEBUG
 struct H2Bvars {
     bvar::Adder<int> h2_unsent_request_count;
     bvar::Adder<int> h2_stream_context_count;
+    bvar::Adder<int64_t> h2_stream_reset_count;
+    bvar::Adder<int64_t> h2_pending_data_bytes;
 
     H2Bvars()
         : h2_unsent_request_count("h2_unsent_request_count")
-        , h2_stream_context_count("h2_stream_context_count") {
+        , h2_stream_context_count("h2_stream_context_count")
+        , h2_stream_reset_count("h2_stream_reset_count")
+        , h2_pending_data_bytes("h2_pending_data_bytes") {
     }
 };
 inline H2Bvars* get_h2_bvars() {
     return butil::get_leaky_singleton<H2Bvars>();
 }
-#endif
 
 class H2UnsentRequest : public SocketMessage, public StreamUserData {
 friend void PackH2Request(butil::IOBuf*, SocketMessage**,
@@ -244,6 +243,8 @@ public:
     
     size_t parsed_length() const { return this->_parsed_length; }
     int stream_id() const { return _stream_id; }
+    bool rst_received() const { return _rst_received; }
+    H2Error rst_error() const { return _rst_error; }
 
     int64_t ReleaseDeferredWindowUpdate() {
         if (_deferred_window_update.load(butil::memory_order_relaxed) == 0) {
@@ -252,6 +253,39 @@ public:
         return _deferred_window_update.exchange(0, 
butil::memory_order_relaxed);
     }
 
+    static int OnSocketFailed(bthread_id_t id, void* data, int error_code);
+
+    void HoldDeferredResponseSocket(Socket* socket, bthread_id_t notify_id) {
+        CHECK_EQ(INVALID_BTHREAD_ID.value,
+                 _deferred_response_id.load(butil::memory_order_relaxed));
+        butil::intrusive_ptr<H2StreamContext>(this).detach();
+        socket->AddIdleFailRef();
+        socket->ReAddress(&_deferred_response_sock);
+        _deferred_response_id.store(notify_id.value, 
butil::memory_order_release);
+    }
+
+    void ReleaseDeferredResponseSocket() {
+        const uint64_t id_val = _deferred_response_id.exchange(
+            INVALID_BTHREAD_ID.value, butil::memory_order_acq_rel);
+        if (id_val != INVALID_BTHREAD_ID.value) {
+            const bthread_id_t id = {id_val};
+            bthread_id_error(id, 0);
+        }
+    }
+
+    void RemoveOneRefForStage2() {
+        ReleaseDeferredResponseSocket();
+        HttpContext::RemoveOneRefForStage2();
+    }
+
+    // @InputMessageBase
+    void DestroyImpl() override {
+        if (!read_body_progressively()) {
+            body().clear();
+        }
+        HttpContext::DestroyImpl();
+    }
+
 #if defined(BRPC_H2_STREAM_STATE)
     H2StreamState state() const { return _state; }
     void SetState(H2StreamState state);
@@ -264,6 +298,10 @@ friend class H2Context;
 #endif
     int _stream_id;
     bool _stream_ended;
+    bool _remote_ended;
+    bool _response_committed;
+    bool _rst_received;
+    H2Error _rst_error;
     butil::atomic<int64_t> _remote_window_left;
     butil::atomic<int64_t> _deferred_window_update;
     uint64_t _correlation_id;
@@ -284,10 +322,18 @@ friend class H2Context;
     // 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::atomic<uint64_t> _deferred_response_id;
+    SocketUniquePtr _deferred_response_sock;
     butil::IOBuf _remaining_header_fragment;
-    // Request body which cannot be sent yet due to remote flow control.
-    // Accessed under H2Context::_stream_mutex.
+    // Request or response body which cannot be sent yet due to remote flow 
control.
+    // Accessed under H2Context::_stream_mutex. Bounded per connection by
+    // -h2_max_pending_data_bytes (or -socket_max_unwritten_bytes when 0);
+    // idle connections with stalled peers that withhold WINDOW_UPDATE can be
+    // reclaimed when ServerOptions::idle_timeout_sec is enabled.
     butil::IOBuf _pending_data;
+    // Encoded gRPC trailer headers waiting for _pending_data to be flushed.
+    // Accessed under H2Context::_stream_mutex.
+    butil::IOBuf _pending_trailer;
 };
 
 StreamCreator* get_h2_global_stream_creator();
@@ -386,19 +432,29 @@ friend void InitFrameHandlers();
     H2ParseResult OnWindowUpdate(butil::IOBufBytesIterator&, const 
H2FrameHead&);
     H2ParseResult OnContinuation(butil::IOBufBytesIterator&, const 
H2FrameHead&);
 
+    H2StreamContext* RemoveStreamLocked(int stream_id);
     H2StreamContext* RemoveStreamAndDeferWU(int stream_id);
     void RemoveGoAwayStreams(int goaway_stream_id, 
std::vector<H2StreamContext*>* out_streams);
 
     H2StreamContext* FindStream(int stream_id);
+    butil::intrusive_ptr<H2StreamContext> FindOpenStream(int stream_id);
+    butil::intrusive_ptr<H2StreamContext> FindHeaderStream(int stream_id);
     // Atomically checks stream and pending-DATA limits, inserts the client
     // stream, and appends DATA allowed by the current remote windows. On
     // success, ownership of sctx is transferred to this context. On failure,
     // no stream, window, or pending-DATA state is changed.
     butil::Status TryToInsertClientStream(
         int stream_id, H2StreamContext*, const butil::IOBuf&, butil::IOBuf*);
+    butil::Status TryToInsertClientStreamLocked(
+        int stream_id, H2StreamContext*, const butil::IOBuf&, butil::IOBuf*);
     void AppendPendingDataLocked(H2StreamContext*, butil::IOBuf*);
-    void ClearPendingData(int stream_id);
+    // Clears any unsent _pending_data / _pending_trailer for stream_id.
+    // Returns true if the stream still had unsent data/trailers (i.e. 
END_STREAM
+    // had not been sent yet).
+    bool ClearPendingData(int stream_id);
     bool FlushPendingData(int stream_id);
+    void AddPendingDataSize(size_t n);
+    void SubPendingDataSize(size_t n);
 
     // True if the connection is established by client, otherwise it's
     // accepted by server.
@@ -410,6 +466,7 @@ friend void InitFrameHandlers();
     int _goaway_stream_id;
     H2Settings _remote_settings;
     bool _remote_settings_received;
+    bool _client_preface_pending;
     H2Settings _local_settings;
     H2Settings _unack_local_settings;
     HPacker _hpacker;
@@ -418,9 +475,14 @@ friend void InitFrameHandlers();
     typedef butil::FlatMap<int, H2StreamContext*> StreamMap;
     mutable butil::Mutex _stream_mutex;
     StreamMap _pending_streams;
+    // Relaxed atomic snapshot of _pending_streams.size(), updated under
+    // _stream_mutex and read lock-free by VolatilePendingStreamSize().
+    butil::atomic<size_t> _volatile_pending_stream_size;
     // Total bytes retained in H2StreamContext::_pending_data on this
-    // connection. Accessed under _stream_mutex.
-    size_t _pending_data_size;
+    // connection. Modified under _stream_mutex; read lock-free via relaxed
+    // atomic load in PendingDataOvercrowded().
+    butil::atomic<size_t> _pending_data_size;
+    int _last_flushed_stream_id;
     butil::atomic<int64_t> _deferred_window_update;
 };
 
diff --git a/src/brpc/policy/http_rpc_protocol.cpp 
b/src/brpc/policy/http_rpc_protocol.cpp
index e4bac8abd..cb7f8d8c6 100644
--- a/src/brpc/policy/http_rpc_protocol.cpp
+++ b/src/brpc/policy/http_rpc_protocol.cpp
@@ -468,12 +468,27 @@ void ProcessHttpResponse(InputMessageBase* msg) {
                 res_body.copy_to(
                     &body_str, std::min((int)res_body.size(),
                                         FLAGS_http_max_error_length));
-                cntl->SetFailed(EHTTP, "HTTP/%d.%d %d %s: %.*s",
-                                res_header->major_version(),
-                                res_header->minor_version(),
-                                static_cast<int>(res_header->status_code()),
-                                res_header->reason_phrase(),
-                                (int)body_str.size(), body_str.c_str());
+                const H2StreamContext* h2_sctx =
+                    is_http2 ? static_cast<const 
H2StreamContext*>(imsg_guard.get()) : nullptr;
+                if (h2_sctx != nullptr && h2_sctx->rst_received()) {
+                    cntl->SetFailed(EHTTP,
+                                    "HTTP/%d.%d %d %s (stream_id=%d terminated 
by RST_STREAM: %s[0x%x]): %.*s",
+                                    res_header->major_version(),
+                                    res_header->minor_version(),
+                                    
static_cast<int>(res_header->status_code()),
+                                    res_header->reason_phrase(),
+                                    h2_sctx->stream_id(),
+                                    H2ErrorToString(h2_sctx->rst_error()),
+                                    static_cast<unsigned 
int>(h2_sctx->rst_error()),
+                                    (int)body_str.size(), body_str.c_str());
+                } else {
+                    cntl->SetFailed(EHTTP, "HTTP/%d.%d %d %s: %.*s",
+                                    res_header->major_version(),
+                                    res_header->minor_version(),
+                                    
static_cast<int>(res_header->status_code()),
+                                    res_header->reason_phrase(),
+                                    (int)body_str.size(), body_str.c_str());
+                }
             } else if (cntl->response() != nullptr &&
                        cntl->response()->GetDescriptor()->field_count() != 0) {
                 cntl->SetFailed(ERESPONSE, "A protobuf response can't be 
parsed"
@@ -492,6 +507,17 @@ void ProcessHttpResponse(InputMessageBase* msg) {
                     res_header->minor_version(),
                     static_cast<int>(res_header->status_code()),
                     res_header->reason_phrase());
+            if (is_http2) {
+                const H2StreamContext* h2_sctx =
+                    static_cast<const H2StreamContext*>(imsg_guard.get());
+                if (h2_sctx->rst_received()) {
+                    butil::string_appendf(
+                        &err, " (stream_id=%d terminated by RST_STREAM: 
%s[0x%x])",
+                        h2_sctx->stream_id(),
+                        H2ErrorToString(h2_sctx->rst_error()),
+                        static_cast<unsigned int>(h2_sctx->rst_error()));
+                }
+            }
             if (!res_body.empty()) {
                 // Use content as error text if it's present. Notice that
                 // content may be binary data, so the size limit is a must.
@@ -868,6 +894,12 @@ HttpResponseSender::~HttpResponseSender() {
     const google::protobuf::Message* res = nullptr != _messages ? 
_messages->Response() : nullptr;
     
     if (cntl->IsCloseConnection()) {
+        if (cntl->http_request().is_http2()) {
+            H2Context* ctx = 
static_cast<H2Context*>(socket->parsing_context());
+            if (ctx != NULL) {
+                ctx->AddAbandonedStream(_h2_stream_id);
+            }
+        }
         socket->SetFailed();
         return;
     }
@@ -1066,6 +1098,12 @@ HttpResponseSender::~HttpResponseSender() {
     }
 
     if (rc != 0) {
+        if (is_http2) {
+            H2Context* ctx = 
static_cast<H2Context*>(socket->parsing_context());
+            if (ctx != NULL) {
+                ctx->AddAbandonedStream(_h2_stream_id);
+            }
+        }
         // EPIPE is common in pooled connections + backup requests.
         const int errcode = errno;
         PLOG_IF(WARNING, errcode != EPIPE) << "Fail to write into " << *socket;
@@ -1077,6 +1115,10 @@ HttpResponseSender::~HttpResponseSender() {
         bthread_id_join(response_id);
         // Do not care about the result of background writing.
         // TODO: this is not sent
+        // Note: Under HTTP/2 with notify_on_success=true, 
H2UnsentResponse::AppendAndDestroySelf
+        // returns OK once HEADERS and the initial window of DATA frames are 
queued, while
+        // remaining DATA / Trailer frames may be deferred until WINDOW_UPDATE 
arrives.
+        // Thus args.sent_us records when the initial response frames are 
written.
         span->set_sent_us(args.sent_us);
     }
 }
@@ -1775,6 +1817,8 @@ void ProcessHttpRequest(InputMessageBase *msg) {
                 MakeRawHttpRequest(&sample->request, &req_header, ep, 
&req_body);
                 sample->submit(start_parse_us);
             }
+        } else {
+            req_body.clear();
         }
     } else {
         if (imsg_guard->read_body_progressively()) {
diff --git a/src/brpc/socket.cpp b/src/brpc/socket.cpp
index 2985a35b6..65c00f58e 100644
--- a/src/brpc/socket.cpp
+++ b/src/brpc/socket.cpp
@@ -477,6 +477,7 @@ Socket::Socket(Forbidden f)
     , _connection_type_for_progressive_read(CONNECTION_TYPE_UNKNOWN)
     , _controller_released_socket(false)
     , _fail_me_at_server_stop(false)
+    , _idle_fail_ref_count(0)
     , _logoff_flag(false)
     , _error_code(0)
     , _pipeline_q(nullptr)
@@ -774,6 +775,7 @@ int Socket::OnCreated(const SocketOptions& options) {
     _controller_released_socket.store(false, butil::memory_order_relaxed);
     // Maybe non-zero for RTMP connections.
     _fail_me_at_server_stop = false;
+    _idle_fail_ref_count.store(0, butil::memory_order_relaxed);
     _logoff_flag.store(false, butil::memory_order_relaxed);
     _error_code = 0;
     _agent_socket_id.store(INVALID_SOCKET_ID, butil::memory_order_relaxed);
@@ -1113,10 +1115,11 @@ int Socket::ReleaseReferenceIfIdle(int idle_seconds) {
     LOG_IF(WARNING, FLAGS_log_idle_connection_close)
         << "Close " << *this << " due to no data transmission for "
         << idle_seconds << " seconds";
-    if (shall_fail_me_at_server_stop()) {
-        // sockets for streaming purposes (say RTMP) are probably referenced
-        // by many places, ReleaseAdditionalReference() cannot notify other
-        // places to release refs, SetFailed() is a must.
+    if (shall_fail_me_at_server_stop() || shall_fail_me_at_idle_timeout()) {
+        // sockets for streaming purposes (say RTMP) or holding deferred
+        // responses are probably referenced by other places,
+        // ReleaseAdditionalReference() cannot notify other places to release
+        // refs, SetFailed() is a must.
         return SetFailed(EUNUSED, "No data transmission for %d seconds",
                          idle_seconds);
     }
diff --git a/src/brpc/socket.h b/src/brpc/socket.h
index 8a1d37229..b947202c8 100644
--- a/src/brpc/socket.h
+++ b/src/brpc/socket.h
@@ -691,6 +691,21 @@ public:
     void fail_me_at_server_stop() { _fail_me_at_server_stop = true; }
     bool shall_fail_me_at_server_stop() const { return 
_fail_me_at_server_stop; }
 
+    // Track references held by deferred responses waiting for peer activity
+    // (such as HTTP/2 WINDOW_UPDATE). Unlike fail_me_at_server_stop(), these
+    // references are allowed to drain gracefully on Server.Stop(), but must
+    // trigger SetFailed() when the socket becomes idle so NotifyOnFailed()
+    // callbacks can clean up buffered state and release the socket.
+    void AddIdleFailRef() {
+        _idle_fail_ref_count.fetch_add(1, butil::memory_order_relaxed);
+    }
+    void ReleaseIdleFailRef() {
+        _idle_fail_ref_count.fetch_sub(1, butil::memory_order_relaxed);
+    }
+    bool shall_fail_me_at_idle_timeout() const {
+        return _idle_fail_ref_count.load(butil::memory_order_relaxed) > 0;
+    }
+
     // Tag the socket so that the response coming back from socket will be
     // parsed progressively. For example: in HTTP, the RPC may end w/o reading
     // the body part fully.
@@ -1024,6 +1039,7 @@ private:
     butil::atomic<bool> _controller_released_socket;
 
     bool _fail_me_at_server_stop;
+    butil::atomic<int32_t> _idle_fail_ref_count;
 
     // Set by SetLogOff
     butil::atomic<bool> _logoff_flag;
diff --git a/test/brpc_grpc_protocol_unittest.cpp 
b/test/brpc_grpc_protocol_unittest.cpp
index 41107b6e5..a71cd5e67 100644
--- a/test/brpc_grpc_protocol_unittest.cpp
+++ b/test/brpc_grpc_protocol_unittest.cpp
@@ -22,9 +22,17 @@
 #include "brpc/server.h"
 #include "brpc/channel.h"
 #include "brpc/grpc.h"
+#include "bthread/bthread.h"
 #include "butil/time.h"
 #include "grpc.pb.h"
 
+namespace brpc {
+namespace policy {
+DECLARE_int32(h2_client_stream_window_size);
+DECLARE_int32(h2_client_connection_window_size);
+} // namespace policy
+} // namespace brpc
+
 int main(int argc, char* argv[]) {
     testing::InitGoogleTest(&argc, argv);
     GFLAGS_NAMESPACE::ParseCommandLineFlags(&argc, &argv, true);
@@ -94,7 +102,18 @@ public:
                 ::test::GrpcResponse* res,
                 ::google::protobuf::Closure* done) override {
         brpc::ClosureGuard done_guard(done);
-        res->set_message(req->message());
+        const std::string& msg = req->message();
+        const std::string prefix = "resp_size:";
+        if (msg.compare(0, prefix.size(), prefix) == 0) {
+            size_t n = static_cast<size_t>(strtoull(msg.c_str() + 
prefix.size(), nullptr, 10));
+            char fill = msg.back();
+            if (fill >= '0' && fill <= '9') {
+                fill = 'r';
+            }
+            res->set_message(std::string(n, fill));
+        } else {
+            res->set_message(msg);
+        }
     }
 };
 
@@ -329,4 +348,108 @@ TEST(GrpcProtocol, 
client_sends_large_request_with_small_remote_window) {
     EXPECT_EQ(request.message(), response.message());
 }
 
+TEST(GrpcProtocol, server_sends_large_response_with_small_remote_window) {
+    GFLAGS_NAMESPACE::FlagSaver flag_saver;
+    // Simulate a gRPC client with a tiny stream window (256 bytes) and
+    // RFC 7540 default 64 KiB connection window receiving a 512 KiB response
+    // to a tiny request (like GetClusterOverallStatus).
+    brpc::policy::FLAGS_h2_client_stream_window_size = 256;
+    brpc::policy::FLAGS_h2_client_connection_window_size = 65535;
+
+    WindowGrpcService service;
+    brpc::Server server;
+    ASSERT_EQ(0, server.AddService(&service, brpc::SERVER_DOESNT_OWN_SERVICE));
+    ASSERT_EQ(0, server.Start("127.0.0.1:0", nullptr));
+
+    brpc::Channel channel;
+    brpc::ChannelOptions channel_options;
+    channel_options.protocol = g_protocol;
+    channel_options.timeout_ms = 10000;
+    ASSERT_EQ(0, channel.Init(server.listen_address(), &channel_options));
+    test::GrpcService_Stub stub(&channel);
+
+    // Warmup RPC so Server receives Client's SETTINGS (stream_window_size=256)
+    {
+        test::GrpcRequest request;
+        test::GrpcResponse response;
+        brpc::Controller cntl;
+        request.set_message("warmup");
+        request.set_gzip(false);
+        request.set_return_error(false);
+        stub.Method(&cntl, &request, &response, nullptr);
+        ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText();
+        ASSERT_EQ("warmup", response.message());
+    }
+
+    // Request a 512 KiB response with a tiny request
+    const size_t resp_size = 512 * 1024;
+    test::GrpcRequest request;
+    test::GrpcResponse response;
+    brpc::Controller cntl;
+    request.set_message(butil::string_printf("resp_size:%zu:A", resp_size));
+    request.set_gzip(false);
+    request.set_return_error(false);
+    stub.Method(&cntl, &request, &response, nullptr);
+    EXPECT_FALSE(cntl.Failed()) << cntl.ErrorText();
+    EXPECT_EQ(std::string(resp_size, 'A'), response.message());
+}
+
+struct ConcurrentGrpcCallArg {
+    test::GrpcService_Stub* stub;
+    size_t resp_size;
+    char fill;
+    bool ok;
+};
+
+static void* RunConcurrentGrpcCall(void* void_arg) {
+    ConcurrentGrpcCallArg* arg = static_cast<ConcurrentGrpcCallArg*>(void_arg);
+    test::GrpcRequest request;
+    test::GrpcResponse response;
+    brpc::Controller cntl;
+    request.set_message(butil::string_printf("resp_size:%zu:%c", 
arg->resp_size, arg->fill));
+    request.set_gzip(false);
+    request.set_return_error(false);
+    arg->stub->Method(&cntl, &request, &response, nullptr);
+    arg->ok = !cntl.Failed() && (response.message() == 
std::string(arg->resp_size, arg->fill));
+    if (cntl.Failed()) {
+        LOG(ERROR) << "Concurrent gRPC failed: " << cntl.ErrorText();
+    }
+    return nullptr;
+}
+
+TEST(GrpcProtocol, concurrent_large_responses_with_small_remote_window) {
+    GFLAGS_NAMESPACE::FlagSaver flag_saver;
+    // 8 concurrent streams each returning 128 KiB (1 MiB total) over a single
+    // HTTP/2 connection with 4 KiB stream window and 64 KiB connection window.
+    brpc::policy::FLAGS_h2_client_stream_window_size = 4096;
+    brpc::policy::FLAGS_h2_client_connection_window_size = 65535;
+
+    WindowGrpcService service;
+    brpc::Server server;
+    ASSERT_EQ(0, server.AddService(&service, brpc::SERVER_DOESNT_OWN_SERVICE));
+    ASSERT_EQ(0, server.Start("127.0.0.1:0", nullptr));
+
+    brpc::Channel channel;
+    brpc::ChannelOptions channel_options;
+    channel_options.protocol = g_protocol;
+    channel_options.timeout_ms = 15000;
+    ASSERT_EQ(0, channel.Init(server.listen_address(), &channel_options));
+    test::GrpcService_Stub stub(&channel);
+
+    const int kNumStreams = 8;
+    bthread_t tids[kNumStreams];
+    ConcurrentGrpcCallArg args[kNumStreams];
+    for (int i = 0; i < kNumStreams; ++i) {
+        args[i].stub = &stub;
+        args[i].resp_size = 128 * 1024;
+        args[i].fill = static_cast<char>('A' + i);
+        args[i].ok = false;
+        ASSERT_EQ(0, bthread_start_background(&tids[i], nullptr, 
RunConcurrentGrpcCall, &args[i]));
+    }
+    for (int i = 0; i < kNumStreams; ++i) {
+        bthread_join(tids[i], nullptr);
+        EXPECT_TRUE(args[i].ok) << "Stream " << i << " failed";
+    }
+}
+
 } // namespace 
diff --git a/test/brpc_h2_unsent_message_unittest.cpp 
b/test/brpc_h2_unsent_message_unittest.cpp
index 0d7b39c3d..35d0f7c5f 100644
--- a/test/brpc_h2_unsent_message_unittest.cpp
+++ b/test/brpc_h2_unsent_message_unittest.cpp
@@ -25,6 +25,7 @@
 #include "bthread/bthread.h"
 #include "butil/atomicops.h"
 #include "brpc/details/hpack.h"
+#include "brpc/server.h"
 #include "brpc/policy/http_rpc_protocol.h"
 #include "brpc/policy/http2_rpc_protocol.h"
 #include "gperftools_helper.h"
@@ -35,11 +36,15 @@ DECLARE_int64(socket_max_unwritten_bytes);
 
 int main(int argc, char* argv[]) {
     testing::InitGoogleTest(&argc, argv);
+    brpc::get_or_new_client_side_messenger();
     return RUN_ALL_TESTS();
 }
 
 namespace {
 
+const uint8_t H2_FLAGS_END_STREAM = 0x01;
+const uint8_t H2_FLAGS_END_HEADERS = 0x04;
+
 brpc::policy::H2FrameHead PopFrame(butil::IOBuf* buf, std::string* payload) {
     char head[brpc::policy::FRAME_HEAD_SIZE];
     CHECK_EQ(sizeof(head), buf->cutn(head, sizeof(head)));
@@ -140,7 +145,7 @@ TEST(H2UnsentMessage, split_request_data_by_remote_window) {
     EXPECT_EQ("e", payload);
     EXPECT_TRUE(out.empty());
     EXPECT_EQ(5u, sctx->_pending_data.size());
-    EXPECT_EQ(5u, ctx->_pending_data_size);
+    EXPECT_EQ(5u, ctx->_pending_data_size.load());
     EXPECT_EQ(1, ctx->_remote_window_left);
     EXPECT_EQ(0, sctx->_remote_window_left);
 
@@ -155,7 +160,7 @@ TEST(H2UnsentMessage, split_request_data_by_remote_window) {
     EXPECT_EQ(0, frame.flags & 0x1);
     EXPECT_EQ("fgh", payload);
     EXPECT_TRUE(out.empty());
-    EXPECT_EQ(2u, ctx->_pending_data_size);
+    EXPECT_EQ(2u, ctx->_pending_data_size.load());
 
     ctx->_remote_window_left.fetch_add(2, butil::memory_order_relaxed);
     sctx->_remote_window_left.fetch_add(2, butil::memory_order_relaxed);
@@ -169,7 +174,7 @@ TEST(H2UnsentMessage, split_request_data_by_remote_window) {
     EXPECT_EQ("ij", payload);
     EXPECT_TRUE(out.empty());
     EXPECT_TRUE(sctx->_pending_data.empty());
-    EXPECT_EQ(0u, ctx->_pending_data_size);
+    EXPECT_EQ(0u, ctx->_pending_data_size.load());
 }
 
 TEST(H2UnsentMessage, request_does_not_fail_when_body_exceeds_window) {
@@ -216,7 +221,7 @@ TEST(H2UnsentMessage, 
request_does_not_fail_when_body_exceeds_window) {
     brpc::policy::H2StreamContext* sctx = ctx->FindStream(1);
     ASSERT_TRUE(sctx != nullptr);
     EXPECT_EQ(7u, sctx->_pending_data.size());
-    EXPECT_EQ(7u, ctx->_pending_data_size);
+    EXPECT_EQ(7u, ctx->_pending_data_size.load());
     EXPECT_EQ(0, ctx->_remote_window_left);
     EXPECT_EQ(0, sctx->_remote_window_left);
 }
@@ -237,16 +242,43 @@ TEST(H2UnsentMessage, 
invalid_window_update_does_not_change_window) {
     ctx->_remote_window_left = max_window_size;
 
     const char increment[] = {0, 0, 0, 1};
-    butil::IOBuf payload;
-    payload.append(increment, sizeof(increment));
-    butil::IOBufBytesIterator it(payload);
-    const brpc::policy::H2FrameHead frame = {
-        4, brpc::policy::H2_FRAME_WINDOW_UPDATE, 0, 0};
-    const brpc::policy::H2ParseResult result = ctx->OnWindowUpdate(it, frame);
-
-    EXPECT_EQ(brpc::H2_FLOW_CONTROL_ERROR, result.error());
-    EXPECT_EQ(max_window_size,
-              ctx->_remote_window_left.load(butil::memory_order_relaxed));
+    {
+        butil::IOBuf payload;
+        payload.append(increment, sizeof(increment));
+        butil::IOBufBytesIterator it(payload);
+        const brpc::policy::H2FrameHead frame = {
+            4, brpc::policy::H2_FRAME_WINDOW_UPDATE, 0, 0};
+        const brpc::policy::H2ParseResult result = ctx->OnWindowUpdate(it, 
frame);
+
+        EXPECT_EQ(brpc::H2_FLOW_CONTROL_ERROR, result.error());
+        EXPECT_EQ(0, result.stream_id());
+        EXPECT_EQ(max_window_size,
+                  ctx->_remote_window_left.load(butil::memory_order_relaxed));
+    }
+
+    // Stream-level WINDOW_UPDATE overflow must return a stream error 
(stream_id = 1)
+    // per RFC 7540 Section 6.9.1 rather than a connection error.
+    brpc::policy::H2StreamContext* sctx =
+        new brpc::policy::H2StreamContext(false);
+    sctx->Init(ctx, 1);
+    ASSERT_EQ(0, ctx->TryToInsertStream(1, sctx));
+    sctx->_remote_window_left = max_window_size;
+    {
+        butil::IOBuf payload;
+        payload.append(increment, sizeof(increment));
+        butil::IOBufBytesIterator it(payload);
+        const brpc::policy::H2FrameHead frame = {
+            4, brpc::policy::H2_FRAME_WINDOW_UPDATE, 0, 1};
+        const brpc::policy::H2ParseResult result = ctx->OnWindowUpdate(it, 
frame);
+
+        EXPECT_EQ(brpc::H2_FLOW_CONTROL_ERROR, result.error());
+        EXPECT_EQ(1, result.stream_id());
+        EXPECT_EQ(max_window_size,
+                  sctx->_remote_window_left.load(butil::memory_order_relaxed));
+    }
+    brpc::policy::H2StreamContext* removed_sctx = 
ctx->RemoveStreamAndDeferWU(1);
+    ASSERT_EQ(sctx, removed_sctx);
+    removed_sctx->RemoveOneRefForStage2();
 }
 
 TEST(H2UnsentMessage, clear_pending_data_releases_overcrowded_buffer) {
@@ -276,7 +308,7 @@ TEST(H2UnsentMessage, 
clear_pending_data_releases_overcrowded_buffer) {
         ctx->TryToInsertClientStream(1, sctx.get(), body, &out).ok());
     brpc::policy::H2StreamContext* inserted_sctx = sctx.release();
     ASSERT_TRUE(out.empty());
-    ASSERT_EQ(body.size(), ctx->_pending_data_size);
+    ASSERT_EQ(body.size(), ctx->_pending_data_size.load());
     EXPECT_TRUE(ctx->PendingDataOvercrowded());
 
     std::unique_ptr<brpc::policy::H2StreamContext> rejected_sctx(
@@ -288,13 +320,14 @@ TEST(H2UnsentMessage, 
clear_pending_data_releases_overcrowded_buffer) {
         3, rejected_sctx.get(), rejected_body, &out);
     EXPECT_EQ(brpc::EOVERCROWDED, rejected.error_code());
     EXPECT_EQ(nullptr, ctx->FindStream(3));
-    EXPECT_EQ(body.size(), ctx->_pending_data_size);
+    EXPECT_EQ(body.size(), ctx->_pending_data_size.load());
 
-    ctx->ClearPendingData(1);
+    EXPECT_TRUE(ctx->ClearPendingData(1));
+    EXPECT_FALSE(ctx->ClearPendingData(1));
     EXPECT_FALSE(ctx->PendingDataOvercrowded());
 
     EXPECT_TRUE(inserted_sctx->_pending_data.empty());
-    EXPECT_EQ(0u, ctx->_pending_data_size);
+    EXPECT_EQ(0u, ctx->_pending_data_size.load());
 }
 
 TEST(H2UnsentMessage, 
append_and_destroy_self_keeps_hpack_state_on_refused_insert) {
@@ -352,7 +385,7 @@ TEST(H2UnsentMessage, request_throughput) {
     brpc::Controller cntl;
     butil::IOBuf request_buf;
     cntl.http_request().uri() = "0.0.0.0:8010/HttpService/Echo";
-    brpc::policy::SerializeHttpRequest(&request_buf, &cntl, nullptr);
+    brpc::policy::SerializeHttpRequest(&request_buf, &cntl, NULL);
 
     brpc::SocketId id;
     brpc::SocketUniquePtr h2_client_sock;
@@ -362,7 +395,7 @@ TEST(H2UnsentMessage, request_throughput) {
     EXPECT_EQ(0, brpc::Socket::Address(id, &h2_client_sock));
 
     brpc::policy::H2Context* ctx =
-        new brpc::policy::H2Context(h2_client_sock.get(), nullptr);
+        new brpc::policy::H2Context(h2_client_sock.get(), NULL);
     CHECK_EQ(ctx->Init(), 0);
     h2_client_sock->initialize_parsing_context(&ctx);
     ctx->_last_sent_stream_id = 0;
@@ -386,6 +419,30 @@ TEST(H2UnsentMessage, request_throughput) {
         << dummy_buf.size() * 1000000L / elapsed << "/s";
 
     // calc H2UnsentResponse throughput
+    brpc::Server server;
+    brpc::SocketId srv_id;
+    brpc::SocketUniquePtr h2_server_sock;
+    brpc::SocketOptions h2_server_options;
+    EXPECT_EQ(0, brpc::Socket::Create(h2_server_options, &srv_id));
+    EXPECT_EQ(0, brpc::Socket::Address(srv_id, &h2_server_sock));
+
+    brpc::policy::H2Context* srv_ctx =
+        new brpc::policy::H2Context(h2_server_sock.get(), &server);
+    CHECK_EQ(srv_ctx->Init(), 0);
+    h2_server_sock->initialize_parsing_context(&srv_ctx);
+    srv_ctx->_unack_local_settings.max_concurrent_streams =
+        std::numeric_limits<uint32_t>::max();
+    srv_ctx->_remote_settings.stream_window_size = 
brpc::H2Settings::MAX_WINDOW_SIZE;
+    srv_ctx->_remote_window_left = brpc::H2Settings::MAX_WINDOW_SIZE;
+    for (int i = 0; i < ntotal; ++i) {
+        const int stream_id = 2 * i + 1;
+        brpc::policy::H2StreamContext* sctx =
+            new brpc::policy::H2StreamContext(false);
+        sctx->Init(srv_ctx, stream_id);
+        sctx->_remote_ended = true;
+        CHECK_EQ(0, srv_ctx->TryToInsertStream(stream_id, sctx));
+    }
+
     dummy_buf.clear();
     start_us = butil::cpuwide_time_us();
     for (int i = 0; i < ntotal; ++i) {
@@ -393,8 +450,9 @@ TEST(H2UnsentMessage, request_throughput) {
         // cntl.response_attachment()
         cntl.http_response().set_content_type("text/plain");
         cntl.response_attachment().append("0123456789abcedef");
-        brpc::policy::H2UnsentResponse* res = 
brpc::policy::H2UnsentResponse::New(&cntl, 0, false);
-        res->AppendAndDestroySelf(&dummy_buf, h2_client_sock.get());
+        brpc::policy::H2UnsentResponse* res =
+            brpc::policy::H2UnsentResponse::New(&cntl, 2 * i + 1, false);
+        res->AppendAndDestroySelf(&dummy_buf, h2_server_sock.get());
     }
     end_us = butil::cpuwide_time_us();
     elapsed = end_us - start_us;
@@ -402,3 +460,722 @@ TEST(H2UnsentMessage, request_throughput) {
         << (ntotal * 1000000L) / elapsed << "/s, data throughput="
         << dummy_buf.size() * 1000000L / elapsed << "/s";
 }
+
+TEST(H2UnsentMessage, 
split_response_data_and_defer_grpc_trailer_by_remote_window) {
+    brpc::Server server;
+    brpc::SocketId id;
+    brpc::SocketUniquePtr sock;
+    brpc::SocketOptions options;
+    ASSERT_EQ(0, brpc::Socket::Create(options, &id));
+    ASSERT_EQ(0, brpc::Socket::Address(id, &sock));
+    ASSERT_TRUE( !sock->CreatedByConnect());
+
+    brpc::policy::H2Context* ctx =
+        new brpc::policy::H2Context(sock.get(), &server);
+    ASSERT_EQ(0, ctx->Init());
+    sock->initialize_parsing_context(&ctx);
+    ctx->_remote_settings.max_frame_size = 4;
+    ctx->_remote_settings.stream_window_size = 5;
+    ctx->_remote_window_left = 6;
+
+    brpc::policy::H2StreamContext* sctx =
+        new brpc::policy::H2StreamContext(false);
+    sctx->Init(ctx, 1);
+    ASSERT_EQ(0, ctx->TryToInsertStream(1, sctx));
+    // Simulate OnEndStream() + Stage 2 ProcessHttpRequest consuming its ref
+    sctx->_remote_ended = true;
+
+    brpc::Controller cntl;
+    cntl.http_response().set_content_type("application/grpc");
+    cntl.response_attachment().append("abcdefghij");
+    brpc::policy::H2UnsentResponse* response =
+        brpc::policy::H2UnsentResponse::New(&cntl, 1, true /* is_grpc */);
+    ASSERT_TRUE(response != nullptr);
+
+    butil::IOBuf out;
+    ASSERT_TRUE(response->AppendAndDestroySelf(&out, sock.get()).ok());
+
+    std::string payload;
+    // Frame 1+: initial HEADERS (+ CONTINUATION if max_frame_size=4 splits it)
+    brpc::policy::H2FrameHead frame = PopFrame(&out, &payload);
+    EXPECT_EQ(brpc::policy::H2_FRAME_HEADERS, frame.type);
+    EXPECT_EQ(1, frame.stream_id);
+    while ((frame.flags & H2_FLAGS_END_HEADERS) == 0) {
+        frame = PopFrame(&out, &payload);
+        EXPECT_EQ(brpc::policy::H2_FRAME_CONTINUATION, frame.type);
+        EXPECT_EQ(1, frame.stream_id);
+    }
+    EXPECT_EQ(0, frame.flags & H2_FLAGS_END_STREAM);
+
+    // Next Frame: DATA "abcd" (4 bytes)
+    frame = PopFrame(&out, &payload);
+    EXPECT_EQ(brpc::policy::H2_FRAME_DATA, frame.type);
+    EXPECT_EQ(4u, frame.payload_size);
+    EXPECT_EQ(0, frame.flags & H2_FLAGS_END_STREAM);
+    EXPECT_EQ("abcd", payload);
+
+    // Next Frame: DATA "e" (1 byte, limited by stream window 5)
+    frame = PopFrame(&out, &payload);
+    EXPECT_EQ(brpc::policy::H2_FRAME_DATA, frame.type);
+    EXPECT_EQ(1u, frame.payload_size);
+    EXPECT_EQ(0, frame.flags & H2_FLAGS_END_STREAM);
+    EXPECT_EQ("e", payload);
+
+    // Trailer must be deferred because 5 bytes of DATA are still pending
+    EXPECT_TRUE(out.empty());
+    EXPECT_EQ(5u, sctx->_pending_data.size());
+    EXPECT_EQ(5u, ctx->_pending_data_size.load());
+    EXPECT_FALSE(sctx->_pending_trailer.empty());
+    EXPECT_EQ(1, ctx->_remote_window_left);
+    EXPECT_EQ(0, sctx->_remote_window_left);
+
+    // Partial window update (+3 bytes): sends "fgh", trailer still deferred
+    ctx->_remote_window_left.fetch_add(3, butil::memory_order_relaxed);
+    sctx->_remote_window_left.fetch_add(3, butil::memory_order_relaxed);
+    {
+        std::unique_lock<butil::Mutex> mu(ctx->_stream_mutex);
+        ctx->AppendPendingDataLocked(sctx, &out);
+    }
+    frame = PopFrame(&out, &payload);
+    EXPECT_EQ(brpc::policy::H2_FRAME_DATA, frame.type);
+    EXPECT_EQ(3u, frame.payload_size);
+    EXPECT_EQ(0, frame.flags & H2_FLAGS_END_STREAM);
+    EXPECT_EQ("fgh", payload);
+    EXPECT_TRUE(out.empty());
+    EXPECT_EQ(2u, ctx->_pending_data_size.load());
+    EXPECT_FALSE(sctx->_pending_trailer.empty());
+
+    // Final window update (+2 bytes): sends "ij" followed by Trailer 
HEADERS(END_STREAM)
+    ctx->_remote_window_left.fetch_add(2, butil::memory_order_relaxed);
+    sctx->_remote_window_left.fetch_add(2, butil::memory_order_relaxed);
+    {
+        std::unique_lock<butil::Mutex> mu(ctx->_stream_mutex);
+        ctx->AppendPendingDataLocked(sctx, &out);
+    }
+    frame = PopFrame(&out, &payload);
+    EXPECT_EQ(brpc::policy::H2_FRAME_DATA, frame.type);
+    EXPECT_EQ(2u, frame.payload_size);
+    EXPECT_EQ(0, frame.flags & H2_FLAGS_END_STREAM);
+    EXPECT_EQ("ij", payload);
+
+    frame = PopFrame(&out, &payload);
+    EXPECT_EQ(brpc::policy::H2_FRAME_HEADERS, frame.type);
+    EXPECT_NE(0, frame.flags & H2_FLAGS_END_STREAM);
+    EXPECT_EQ(1, frame.stream_id);
+    while ((frame.flags & H2_FLAGS_END_HEADERS) == 0) {
+        frame = PopFrame(&out, &payload);
+        EXPECT_EQ(brpc::policy::H2_FRAME_CONTINUATION, frame.type);
+        EXPECT_EQ(1, frame.stream_id);
+    }
+    EXPECT_TRUE(out.empty());
+    EXPECT_EQ(0u, ctx->_pending_data_size.load());
+}
+
+TEST(H2UnsentMessage, deferred_trailer_does_not_corrupt_cross_stream_hpack) {
+    brpc::Server server;
+    brpc::SocketId id;
+    brpc::SocketUniquePtr sock;
+    brpc::SocketOptions options;
+    ASSERT_EQ(0, brpc::Socket::Create(options, &id));
+    ASSERT_EQ(0, brpc::Socket::Address(id, &sock));
+
+    brpc::policy::H2Context* server_ctx =
+        new brpc::policy::H2Context(sock.get(), &server);
+    ASSERT_EQ(0, server_ctx->Init());
+    sock->initialize_parsing_context(&server_ctx);
+    server_ctx->_remote_settings.stream_window_size = 0;
+    server_ctx->_remote_window_left = 100;
+
+    // Stream 1: window is 0, so its DATA and gRPC Trailer are deferred
+    brpc::policy::H2StreamContext* sctx1 =
+        new brpc::policy::H2StreamContext(false);
+    sctx1->Init(server_ctx, 1);
+    ASSERT_EQ(0, server_ctx->TryToInsertStream(1, sctx1));
+    sctx1->_remote_ended = true;
+
+    // Stream 3: has sufficient window (20 bytes), sends HEADERS + DATA + 
Trailer immediately
+    brpc::policy::H2StreamContext* sctx3 =
+        new brpc::policy::H2StreamContext(false);
+    sctx3->Init(server_ctx, 3);
+    ASSERT_EQ(0, server_ctx->TryToInsertStream(3, sctx3));
+    sctx3->_remote_window_left = 20;
+    sctx3->_remote_ended = true;
+
+    butil::IOBuf wire_buf;
+    {
+        brpc::Controller cntl1;
+        cntl1.http_response().set_content_type("application/grpc");
+        cntl1.http_response().SetHeader("x-custom-stream", "stream-one");
+        cntl1.response_attachment().append("body1");
+        brpc::policy::H2UnsentResponse* res1 =
+            brpc::policy::H2UnsentResponse::New(&cntl1, 1, true);
+        ASSERT_TRUE(res1->AppendAndDestroySelf(&wire_buf, sock.get()).ok());
+    }
+    {
+        brpc::Controller cntl3;
+        cntl3.http_response().set_content_type("application/grpc");
+        cntl3.http_response().SetHeader("x-custom-stream", "stream-three");
+        cntl3.response_attachment().append("body3");
+        brpc::policy::H2UnsentResponse* res3 =
+            brpc::policy::H2UnsentResponse::New(&cntl3, 3, true);
+        ASSERT_TRUE(res3->AppendAndDestroySelf(&wire_buf, sock.get()).ok());
+    }
+    EXPECT_EQ(nullptr, server_ctx->FindStream(3));
+    ASSERT_NE(nullptr, server_ctx->FindStream(1));
+
+    // Now replenish Stream 1's window and flush its DATA + deferred Trailer
+    sctx1->_remote_window_left.fetch_add(10, butil::memory_order_relaxed);
+    {
+        std::unique_lock<butil::Mutex> mu(server_ctx->_stream_mutex);
+        server_ctx->AppendPendingDataLocked(sctx1, &wire_buf);
+    }
+
+    // Decode all HEADERS frames in wire transmission order using a client 
HPacker
+    brpc::HPacker client_hpacker;
+    ASSERT_EQ(0, 
client_hpacker.Init(brpc::H2Settings::DEFAULT_HEADER_TABLE_SIZE));
+    int trailer_stream1_count = 0;
+    while (!wire_buf.empty()) {
+        std::string payload;
+        brpc::policy::H2FrameHead frame = PopFrame(&wire_buf, &payload);
+        if (frame.type == brpc::policy::H2_FRAME_HEADERS) {
+            butil::IOBuf hdr_buf;
+            hdr_buf.append(payload);
+            butil::IOBufBytesIterator it(hdr_buf);
+            while (it != NULL) {
+                brpc::HPacker::Header h;
+                ASSERT_GT(client_hpacker.Decode(it, &h), 0);
+                if (frame.stream_id == 1 &&
+                    (frame.flags & H2_FLAGS_END_STREAM) &&
+                    h.name == "grpc-status") {
+                    EXPECT_EQ("0", h.value);
+                    ++trailer_stream1_count;
+                }
+            }
+        }
+    }
+    EXPECT_EQ(1, trailer_stream1_count);
+}
+
+TEST(H2UnsentMessage, server_pending_response_cleaned_up_on_rst_stream) {
+    brpc::Server server;
+    brpc::SocketId id;
+    brpc::SocketUniquePtr sock;
+    brpc::SocketOptions options;
+    ASSERT_EQ(0, brpc::Socket::Create(options, &id));
+    ASSERT_EQ(0, brpc::Socket::Address(id, &sock));
+
+    brpc::policy::H2Context* ctx =
+        new brpc::policy::H2Context(sock.get(), &server);
+    ASSERT_EQ(0, ctx->Init());
+    sock->initialize_parsing_context(&ctx);
+    ctx->_remote_window_left = 0;
+
+    brpc::policy::H2StreamContext* sctx =
+        new brpc::policy::H2StreamContext(false);
+    sctx->Init(ctx, 1);
+    ASSERT_EQ(0, ctx->TryToInsertStream(1, sctx));
+    sctx->_remote_ended = true;
+
+    brpc::Controller cntl;
+    cntl.http_response().set_content_type("application/grpc");
+    cntl.response_attachment().append("0123456789");
+    brpc::policy::H2UnsentResponse* response =
+        brpc::policy::H2UnsentResponse::New(&cntl, 1, true);
+    butil::IOBuf out;
+    ASSERT_TRUE(response->AppendAndDestroySelf(&out, sock.get()).ok());
+    EXPECT_EQ(10u, ctx->_pending_data_size.load());
+    ASSERT_NE(nullptr, ctx->FindStream(1));
+
+    // Simulate client sending RST_STREAM(CANCEL) for stream 1
+    const char rst_payload[] = {0, 0, 0, 8 /* H2_CANCEL */};
+    butil::IOBuf payload_buf;
+    payload_buf.append(rst_payload, sizeof(rst_payload));
+    butil::IOBufBytesIterator it(payload_buf);
+    const brpc::policy::H2FrameHead rst_head = {
+        4, brpc::policy::H2_FRAME_RST_STREAM, 0, 1};
+    brpc::policy::H2ParseResult res = ctx->OnResetStream(it, rst_head);
+    EXPECT_TRUE(res.is_ok());
+    EXPECT_EQ(nullptr, ctx->FindStream(1));
+    EXPECT_EQ(0u, ctx->_pending_data_size.load());
+}
+
+TEST(H2UnsentMessage, server_pending_response_overcrowded_sends_rst_stream) {
+    brpc::Server server;
+    brpc::SocketId id;
+    brpc::SocketUniquePtr sock;
+    brpc::SocketOptions options;
+    ASSERT_EQ(0, brpc::Socket::Create(options, &id));
+    ASSERT_EQ(0, brpc::Socket::Address(id, &sock));
+
+    brpc::policy::H2Context* ctx =
+        new brpc::policy::H2Context(sock.get(), &server);
+    ASSERT_EQ(0, ctx->Init());
+    sock->initialize_parsing_context(&ctx);
+    ctx->_remote_window_left = 0;
+
+    GFLAGS_NAMESPACE::FlagSaver flag_saver;
+    brpc::FLAGS_socket_max_unwritten_bytes = 8;
+
+    // Stream 1 queues 8 bytes, reaching socket_max_unwritten_bytes
+    brpc::policy::H2StreamContext* sctx1 =
+        new brpc::policy::H2StreamContext(false);
+    sctx1->Init(ctx, 1);
+    ASSERT_EQ(0, ctx->TryToInsertStream(1, sctx1));
+    sctx1->_remote_ended = true;
+
+    brpc::Controller cntl1;
+    cntl1.response_attachment().append("12345678");
+    brpc::policy::H2UnsentResponse* res1 =
+        brpc::policy::H2UnsentResponse::New(&cntl1, 1, false);
+    butil::IOBuf out1;
+    ASSERT_TRUE(res1->AppendAndDestroySelf(&out1, sock.get()).ok());
+    EXPECT_EQ(8u, ctx->_pending_data_size.load());
+    EXPECT_TRUE(ctx->PendingDataOvercrowded());
+
+    // Stream 3 arrives while overcrowded -> emits 
RST_STREAM(ENHANCE_YOUR_CALM)
+    brpc::policy::H2StreamContext* sctx3 =
+        new brpc::policy::H2StreamContext(false);
+    sctx3->Init(ctx, 3);
+    ASSERT_EQ(0, ctx->TryToInsertStream(3, sctx3));
+    sctx3->_remote_ended = true;
+
+    brpc::Controller cntl3;
+    cntl3.response_attachment().append("abcdefgh");
+    brpc::policy::H2UnsentResponse* res3 =
+        brpc::policy::H2UnsentResponse::New(&cntl3, 3, true);
+    butil::IOBuf out3;
+    ASSERT_TRUE(res3->AppendAndDestroySelf(&out3, sock.get()).ok());
+
+    std::string payload;
+    brpc::policy::H2FrameHead frame = PopFrame(&out3, &payload);
+    EXPECT_EQ(brpc::policy::H2_FRAME_RST_STREAM, frame.type);
+    EXPECT_EQ(3, frame.stream_id);
+    EXPECT_EQ(4u, frame.payload_size);
+    EXPECT_TRUE(out3.empty());
+    EXPECT_EQ(nullptr, ctx->FindStream(3));
+    EXPECT_EQ(8u, ctx->_pending_data_size.load());
+}
+
+TEST(H2UnsentMessage, flush_pending_data_round_robin_fairness) {
+    brpc::Server server;
+    int fds[2];
+    ASSERT_EQ(0, pipe(fds));
+    brpc::SocketId id;
+    brpc::SocketUniquePtr sock;
+    brpc::SocketOptions options;
+    options.fd = fds[1];
+    ASSERT_EQ(0, brpc::Socket::Create(options, &id));
+    ASSERT_EQ(0, brpc::Socket::Address(id, &sock));
+
+    brpc::policy::H2Context* ctx =
+        new brpc::policy::H2Context(sock.get(), &server);
+    ASSERT_EQ(0, ctx->Init());
+    sock->initialize_parsing_context(&ctx);
+    ctx->_remote_settings.max_frame_size = 4;
+    ctx->_remote_settings.stream_window_size = 16;
+    ctx->_remote_window_left = 0;
+
+    for (int stream_id : {1, 3}) {
+        brpc::policy::H2StreamContext* sctx =
+            new brpc::policy::H2StreamContext(false);
+        sctx->Init(ctx, stream_id);
+        ASSERT_EQ(0, ctx->TryToInsertStream(stream_id, sctx));
+        sctx->_remote_ended = true;
+
+        brpc::Controller cntl;
+        cntl.response_attachment().append(stream_id == 1 ? "11112222" : 
"33334444");
+        brpc::policy::H2UnsentResponse* res =
+            brpc::policy::H2UnsentResponse::New(&cntl, stream_id, false);
+        butil::IOBuf hdr_out;
+        ASSERT_TRUE(res->AppendAndDestroySelf(&hdr_out, sock.get()).ok());
+    }
+    EXPECT_EQ(16u, ctx->_pending_data_size.load());
+
+    // Replenish connection window by 4 bytes -> FlushPendingData flushes 
stream 1 ("1111")
+    ctx->_remote_window_left.fetch_add(4, butil::memory_order_relaxed);
+    ASSERT_TRUE(ctx->FlushPendingData(0));
+    EXPECT_EQ(1, ctx->_last_flushed_stream_id);
+    EXPECT_EQ(12u, ctx->_pending_data_size.load());
+
+    // Replenish connection window by 4 bytes -> Round-robin flushes stream 3 
("3333") next!
+    ctx->_remote_window_left.fetch_add(4, butil::memory_order_relaxed);
+    ASSERT_TRUE(ctx->FlushPendingData(0));
+    EXPECT_EQ(3, ctx->_last_flushed_stream_id);
+    EXPECT_EQ(8u, ctx->_pending_data_size.load());
+
+    // Replenish connection window by 8 bytes -> Flushes remaining stream 1 
("2222") and stream 3 ("4444")
+    ctx->_remote_window_left.fetch_add(8, butil::memory_order_relaxed);
+    ASSERT_TRUE(ctx->FlushPendingData(0));
+    EXPECT_EQ(0u, ctx->_pending_data_size.load());
+    EXPECT_EQ(nullptr, ctx->FindStream(1));
+    EXPECT_EQ(nullptr, ctx->FindStream(3));
+
+    butil::IOPortal pipe_buf;
+    ASSERT_GT(pipe_buf.append_from_file_descriptor(fds[0], 1024), 0);
+    std::string payload;
+    brpc::policy::H2FrameHead f1 = PopFrame(&pipe_buf, &payload);
+    EXPECT_EQ(1, f1.stream_id);
+    EXPECT_EQ("1111", payload);
+    brpc::policy::H2FrameHead f2 = PopFrame(&pipe_buf, &payload);
+    EXPECT_EQ(3, f2.stream_id);
+    EXPECT_EQ("3333", payload);
+    brpc::policy::H2FrameHead f3 = PopFrame(&pipe_buf, &payload);
+    EXPECT_EQ(1, f3.stream_id);
+    EXPECT_EQ("2222", payload);
+    EXPECT_NE(0, f3.flags & H2_FLAGS_END_STREAM);
+    brpc::policy::H2FrameHead f4 = PopFrame(&pipe_buf, &payload);
+    EXPECT_EQ(3, f4.stream_id);
+    EXPECT_EQ("4444", payload);
+    EXPECT_NE(0, f4.flags & H2_FLAGS_END_STREAM);
+    close(fds[0]);
+}
+
+TEST(H2UnsentMessage, 
client_rejected_request_does_not_corrupt_preface_or_hpack) {
+    GFLAGS_NAMESPACE::FlagSaver flag_saver;
+    brpc::FLAGS_socket_max_unwritten_bytes = 4;
+
+    brpc::SocketId client_id;
+    brpc::SocketUniquePtr client_sock;
+    brpc::SocketOptions client_options;
+    client_options.user = brpc::get_client_side_messenger();
+    // CreatedByConnect() is true when remote_side is non-empty.
+    ASSERT_EQ(0, butil::str2endpoint("127.0.0.1:8010", 
&client_options.remote_side));
+    ASSERT_EQ(0, brpc::Socket::Create(client_options, &client_id));
+    ASSERT_EQ(0, brpc::Socket::Address(client_id, &client_sock));
+
+    // First request on brand-new connection exceeds (initial_window=65535 + 
max_unwritten=4).
+    brpc::Controller cntl1;
+    cntl1.http_request().uri() = "http://example.com/echo";;
+    cntl1.http_request().SetHeader("x-custom-dynamic-header", 
"first-rejected-value");
+    cntl1.request_attachment().append(std::string(65535 + 16, 'x'));
+    brpc::policy::H2UnsentRequest* req1 =
+        brpc::policy::H2UnsentRequest::New(&cntl1);
+    butil::IOBuf out1;
+    butil::Status st1 = req1->AppendAndDestroySelf(&out1, client_sock.get());
+    EXPECT_EQ(brpc::EOVERCROWDED, st1.error_code());
+    EXPECT_TRUE(out1.empty());
+
+    brpc::policy::H2Context* client_ctx =
+        static_cast<brpc::policy::H2Context*>(client_sock->parsing_context());
+    ASSERT_NE(nullptr, client_ctx);
+#if defined(UNIT_TEST)
+    const int initial_stream_id = 0x7fffffff - 10000;
+#else
+    const int initial_stream_id = 1;
+#endif
+    // Stream ID must not have been consumed, and preface must still be 
pending.
+    EXPECT_EQ(initial_stream_id, client_ctx->_last_sent_stream_id);
+    EXPECT_TRUE(client_ctx->_client_preface_pending);
+
+    // Second request fits within window and must emit connection preface + 
SETTINGS + valid HPACK.
+    brpc::Controller cntl2;
+    cntl2.http_request().uri() = "http://example.com/echo";;
+    cntl2.http_request().SetHeader("x-custom-dynamic-header", 
"first-rejected-value");
+    cntl2.request_attachment().append("ok");
+    brpc::policy::H2UnsentRequest* req2 =
+        brpc::policy::H2UnsentRequest::New(&cntl2);
+    butil::IOBuf out2;
+    butil::Status st2 = req2->AppendAndDestroySelf(&out2, client_sock.get());
+    ASSERT_TRUE(st2.ok()) << st2;
+    EXPECT_EQ(initial_stream_id + 2, client_ctx->_last_sent_stream_id);
+    EXPECT_FALSE(client_ctx->_client_preface_pending);
+
+    // Verify a fresh server H2Context can parse out2 (including preface, 
SETTINGS, and HPACK headers).
+    brpc::Server server;
+    int fds[2];
+    ASSERT_EQ(0, pipe(fds));
+    brpc::SocketId server_id;
+    brpc::SocketUniquePtr server_sock;
+    brpc::SocketOptions server_options;
+    server_options.fd = fds[1];
+    ASSERT_EQ(0, brpc::Socket::Create(server_options, &server_id));
+    ASSERT_EQ(0, brpc::Socket::Address(server_id, &server_sock));
+
+    brpc::ParseResult pr = brpc::policy::ParseH2Message(
+        &out2, server_sock.get(), false, &server);
+    ASSERT_TRUE(pr.is_ok());
+    ASSERT_NE(nullptr, pr.message());
+    auto* received_sctx =
+        static_cast<brpc::policy::H2StreamContext*>(pr.message());
+    EXPECT_EQ(initial_stream_id, received_sctx->stream_id());
+    const std::string* hdr_val =
+        received_sctx->header().GetHeader("x-custom-dynamic-header");
+    ASSERT_NE(nullptr, hdr_val);
+    EXPECT_EQ("first-rejected-value", *hdr_val);
+    received_sctx->Destroy();
+    close(fds[0]);
+}
+
+TEST(H2UnsentMessage, 
rejected_headers_on_remote_ended_stream_preserves_frame_and_hpack) {
+    brpc::Server server;
+    int fds[2];
+    ASSERT_EQ(0, pipe(fds));
+    brpc::SocketId server_id;
+    brpc::SocketUniquePtr server_sock;
+    brpc::SocketOptions server_options;
+    server_options.fd = fds[1];
+    ASSERT_EQ(0, brpc::Socket::Create(server_options, &server_id));
+    ASSERT_EQ(0, brpc::Socket::Address(server_id, &server_sock));
+
+    brpc::policy::H2Context* srv_ctx =
+        new brpc::policy::H2Context(server_sock.get(), &server);
+    ASSERT_EQ(0, srv_ctx->Init());
+    server_sock->initialize_parsing_context(&srv_ctx);
+    srv_ctx->_conn_state = brpc::policy::H2_CONNECTION_READY;
+
+    // Insert Stream 1 as already remote-ended (retained while response is 
pending).
+    brpc::policy::H2StreamContext* sctx1 =
+        new brpc::policy::H2StreamContext(false);
+    sctx1->Init(srv_ctx, 1);
+    sctx1->_remote_ended = true;
+    ASSERT_EQ(0, srv_ctx->TryToInsertStream(1, sctx1));
+
+    brpc::HPacker client_hpacker;
+    ASSERT_EQ(0, 
client_hpacker.Init(brpc::H2Settings::DEFAULT_HEADER_TABLE_SIZE));
+    brpc::HPackOptions hpack_opt;
+    hpack_opt.encode_name = false;
+    hpack_opt.encode_value = false;
+
+    // Encode a fragmented header block for Stream 1 across HEADERS (without
+    // END_HEADERS) + CONTINUATION (with END_HEADERS) that inserts entries into
+    // the HPACK dynamic table.
+    butil::IOBufAppender appender1_part1;
+    client_hpacker.Encode(
+        &appender1_part1, brpc::HPacker::Header("x-rejected-first", 
"first-val"), hpack_opt);
+    butil::IOBuf frag1_part1;
+    appender1_part1.move_to(frag1_part1);
+
+    butil::IOBufAppender appender1_part2;
+    client_hpacker.Encode(
+        &appender1_part2, brpc::HPacker::Header("x-rejected-dyn", 
"dyn-value-123"), hpack_opt);
+    butil::IOBuf frag1_part2;
+    appender1_part2.move_to(frag1_part2);
+
+    butil::IOBuf wire;
+    char headbuf[brpc::policy::FRAME_HEAD_SIZE];
+    brpc::policy::SerializeFrameHead(
+        headbuf, frag1_part1.size(), brpc::policy::H2_FRAME_HEADERS, 0, 1);
+    wire.append(headbuf, sizeof(headbuf));
+    wire.append(frag1_part1);
+
+    brpc::policy::SerializeFrameHead(
+        headbuf, frag1_part2.size(), brpc::policy::H2_FRAME_CONTINUATION,
+        H2_FLAGS_END_HEADERS, 1);
+    wire.append(headbuf, sizeof(headbuf));
+    wire.append(frag1_part2);
+
+    // Immediately follow with a new valid request on Stream 3 referencing the
+    // dynamic table entry added by the rejected Stream 1 header block.
+    butil::IOBufAppender appender3;
+    client_hpacker.Encode(&appender3, brpc::HPacker::Header(":method", "GET"), 
hpack_opt);
+    client_hpacker.Encode(&appender3, brpc::HPacker::Header(":scheme", 
"http"), hpack_opt);
+    client_hpacker.Encode(&appender3, brpc::HPacker::Header(":path", 
"/EchoService/Echo"), hpack_opt);
+    client_hpacker.Encode(
+        &appender3, brpc::HPacker::Header("x-rejected-dyn", "dyn-value-123"), 
hpack_opt);
+    butil::IOBuf hdr_block3;
+    appender3.move_to(hdr_block3);
+
+    brpc::policy::SerializeFrameHead(
+        headbuf, hdr_block3.size(), brpc::policy::H2_FRAME_HEADERS,
+        H2_FLAGS_END_HEADERS | H2_FLAGS_END_STREAM, 3);
+    wire.append(headbuf, sizeof(headbuf));
+    wire.append(hdr_block3);
+
+    // Parse the combined wire buffer: Stream 1 must be reset with 
STREAM_CLOSED
+    // after CONTINUATION, and Stream 3 must parse cleanly with the updated 
HPACK table.
+    brpc::ParseResult pr = brpc::policy::ParseH2Message(
+        &wire, server_sock.get(), false, &server);
+    ASSERT_TRUE(pr.is_ok());
+    ASSERT_NE(nullptr, pr.message());
+    EXPECT_EQ(nullptr, srv_ctx->FindStream(1));
+
+    auto* sctx3 = static_cast<brpc::policy::H2StreamContext*>(pr.message());
+    EXPECT_EQ(3, sctx3->stream_id());
+    const std::string* val = sctx3->header().GetHeader("x-rejected-dyn");
+    ASSERT_NE(nullptr, val);
+    EXPECT_EQ("dyn-value-123", *val);
+    sctx3->Destroy();
+    close(fds[0]);
+}
+
+TEST(H2UnsentMessage, 
deferred_response_notify_registration_failure_resets_stream) {
+    brpc::Server server;
+    int fds[2];
+    ASSERT_EQ(0, pipe(fds));
+    brpc::SocketId server_id;
+    brpc::SocketUniquePtr server_sock;
+    brpc::SocketOptions server_options;
+    server_options.fd = fds[1];
+    ASSERT_EQ(0, brpc::Socket::Create(server_options, &server_id));
+    ASSERT_EQ(0, brpc::Socket::Address(server_id, &server_sock));
+
+    brpc::policy::H2Context* srv_ctx =
+        new brpc::policy::H2Context(server_sock.get(), &server);
+    ASSERT_EQ(0, srv_ctx->Init());
+    server_sock->initialize_parsing_context(&srv_ctx);
+    srv_ctx->_conn_state = brpc::policy::H2_CONNECTION_READY;
+    srv_ctx->_remote_settings.stream_window_size = 0;
+
+    brpc::policy::H2StreamContext* sctx =
+        new brpc::policy::H2StreamContext(false);
+    sctx->Init(srv_ctx, 1);
+    sctx->_remote_ended = true;
+    sctx->_deferred_window_update.store(8, butil::memory_order_relaxed);
+    ASSERT_EQ(0, srv_ctx->TryToInsertStream(1, sctx));
+
+    // Saturate server_sock->_id_wait_list so Socket::NotifyOnFailed() fails 
with
+    // EAGAIN while server_sock itself remains healthy 
(!server_sock->Failed()).
+    bthread_id_t dummy_id = INVALID_BTHREAD_ID;
+    ASSERT_EQ(0, bthread_id_create(&dummy_id, NULL, NULL));
+    {
+        BAIDU_SCOPED_LOCK(server_sock->_id_wait_list_mutex);
+        int consecutive_eagain = 0;
+        while (consecutive_eagain < 26000) {
+            const int rc =
+                bthread_id_list_add(&server_sock->_id_wait_list, dummy_id);
+            if (rc == EAGAIN) {
+                ++consecutive_eagain;
+            } else {
+                ASSERT_EQ(0, rc);
+                consecutive_eagain = 0;
+            }
+        }
+    }
+    ASSERT_FALSE(server_sock->Failed());
+
+    brpc::Controller cntl;
+    cntl.http_response().set_status_code(brpc::HTTP_STATUS_OK);
+    cntl.response_attachment().append("hello world");
+    brpc::policy::H2UnsentResponse* resp =
+        brpc::policy::H2UnsentResponse::New(&cntl, 1, false);
+    ASSERT_NE(nullptr, resp);
+
+    butil::IOBuf out;
+    butil::Status st = resp->AppendAndDestroySelf(&out, server_sock.get());
+    ASSERT_TRUE(st.ok()) << st;
+
+    EXPECT_FALSE(server_sock->Failed());
+    EXPECT_EQ(nullptr, srv_ctx->FindStream(1));
+    EXPECT_EQ(0u, srv_ctx->_pending_data_size.load());
+    EXPECT_EQ(0, server_sock->_idle_fail_ref_count.load());
+
+    std::string payload;
+    brpc::policy::H2FrameHead frame = PopFrame(&out, &payload);
+    EXPECT_EQ(brpc::policy::H2_FRAME_HEADERS, frame.type);
+    EXPECT_EQ(1, frame.stream_id);
+    EXPECT_EQ(0, frame.flags & H2_FLAGS_END_STREAM);
+
+    frame = PopFrame(&out, &payload);
+    EXPECT_EQ(brpc::policy::H2_FRAME_RST_STREAM, frame.type);
+    EXPECT_EQ(1, frame.stream_id);
+    ASSERT_EQ(4u, payload.size());
+    const uint32_t rst_err =
+        (static_cast<uint8_t>(payload[0]) << 24) |
+        (static_cast<uint8_t>(payload[1]) << 16) |
+        (static_cast<uint8_t>(payload[2]) << 8) |
+        static_cast<uint8_t>(payload[3]);
+    EXPECT_EQ(static_cast<uint32_t>(brpc::H2_INTERNAL_ERROR), rst_err);
+
+    frame = PopFrame(&out, &payload);
+    EXPECT_EQ(brpc::policy::H2_FRAME_WINDOW_UPDATE, frame.type);
+    EXPECT_EQ(0, frame.stream_id);
+    EXPECT_TRUE(out.empty());
+
+    EXPECT_EQ(0, bthread_id_error(dummy_id, 0));
+    close(fds[0]);
+}
+
+TEST(H2UnsentMessage, early_response_migrates_deferred_window_update) {
+    brpc::Server server;
+    int fds[2];
+    ASSERT_EQ(0, pipe(fds));
+    brpc::SocketId server_id;
+    brpc::SocketUniquePtr server_sock;
+    brpc::SocketOptions server_options;
+    server_options.fd = fds[1];
+    ASSERT_EQ(0, brpc::Socket::Create(server_options, &server_id));
+    ASSERT_EQ(0, brpc::Socket::Address(server_id, &server_sock));
+
+    brpc::policy::H2Context* srv_ctx =
+        new brpc::policy::H2Context(server_sock.get(), &server);
+    ASSERT_EQ(0, srv_ctx->Init());
+    server_sock->initialize_parsing_context(&srv_ctx);
+    srv_ctx->_conn_state = brpc::policy::H2_CONNECTION_READY;
+
+    // Case 1: Immediate response completion in 
H2UnsentResponse::AppendAndDestroySelf
+    // when the request has not ended yet (_remote_ended == false) must migrate
+    // sctx->_deferred_window_update to the connection and emit 
connection-level
+    // WINDOW_UPDATE in `out'.
+    brpc::policy::H2StreamContext* sctx1 =
+        new brpc::policy::H2StreamContext(true);
+    sctx1->Init(srv_ctx, 1);
+    sctx1->_remote_ended = false;
+    sctx1->_deferred_window_update.store(1234, butil::memory_order_relaxed);
+    ASSERT_EQ(0, srv_ctx->TryToInsertStream(1, sctx1));
+
+    {
+        brpc::Controller cntl;
+        cntl.http_response().set_status_code(brpc::HTTP_STATUS_OK);
+        cntl.response_attachment().append("ok");
+        brpc::policy::H2UnsentResponse* resp =
+            brpc::policy::H2UnsentResponse::New(&cntl, 1, false);
+        ASSERT_NE(nullptr, resp);
+        butil::IOBuf out;
+        ASSERT_TRUE(resp->AppendAndDestroySelf(&out, server_sock.get()).ok());
+        EXPECT_EQ(nullptr, srv_ctx->FindStream(1));
+
+        std::string payload;
+        brpc::policy::H2FrameHead frame = PopFrame(&out, &payload);
+        EXPECT_EQ(brpc::policy::H2_FRAME_HEADERS, frame.type);
+        frame = PopFrame(&out, &payload);
+        EXPECT_EQ(brpc::policy::H2_FRAME_DATA, frame.type);
+        EXPECT_NE(0, frame.flags & H2_FLAGS_END_STREAM);
+        frame = PopFrame(&out, &payload);
+        EXPECT_EQ(brpc::policy::H2_FRAME_WINDOW_UPDATE, frame.type);
+        EXPECT_EQ(0, frame.stream_id);
+        ASSERT_EQ(4u, payload.size());
+        const uint32_t wu_inc =
+            (static_cast<uint8_t>(payload[0]) << 24) |
+            (static_cast<uint8_t>(payload[1]) << 16) |
+            (static_cast<uint8_t>(payload[2]) << 8) |
+            static_cast<uint8_t>(payload[3]);
+        EXPECT_EQ(1234u, wu_inc);
+        EXPECT_TRUE(out.empty());
+    }
+
+    // Case 2: Deferred response completion in H2Context::FlushPendingData when
+    // the request has not ended yet (_remote_ended == false) must also migrate
+    // sctx->_deferred_window_update to the connection.
+    srv_ctx->_remote_settings.stream_window_size = 0;
+    brpc::policy::H2StreamContext* sctx3 =
+        new brpc::policy::H2StreamContext(true);
+    sctx3->Init(srv_ctx, 3);
+    sctx3->_remote_ended = false;
+    sctx3->_deferred_window_update.store(4321, butil::memory_order_relaxed);
+    ASSERT_EQ(0, srv_ctx->TryToInsertStream(3, sctx3));
+
+    {
+        brpc::Controller cntl;
+        cntl.http_response().set_status_code(brpc::HTTP_STATUS_OK);
+        cntl.response_attachment().append("deferred");
+        brpc::policy::H2UnsentResponse* resp =
+            brpc::policy::H2UnsentResponse::New(&cntl, 3, false);
+        ASSERT_NE(nullptr, resp);
+        butil::IOBuf out;
+        ASSERT_TRUE(resp->AppendAndDestroySelf(&out, server_sock.get()).ok());
+        ASSERT_NE(nullptr, srv_ctx->FindStream(3));
+        // Clear the connection WU flushed during AppendAndDestroySelf, then
+        // restore sctx3's request-side deferred window credit to verify
+        // FlushPendingData's own migration path.
+        EXPECT_EQ(0, srv_ctx->_deferred_window_update.exchange(
+                         0, butil::memory_order_relaxed));
+        sctx3->_deferred_window_update.store(4321, 
butil::memory_order_relaxed);
+
+        sctx3->_remote_window_left.store(64, butil::memory_order_relaxed);
+        ASSERT_TRUE(srv_ctx->FlushPendingData(3));
+        EXPECT_EQ(nullptr, srv_ctx->FindStream(3));
+        EXPECT_EQ(4321, srv_ctx->_deferred_window_update.exchange(
+                            0, butil::memory_order_relaxed));
+    }
+    close(fds[0]);
+}
diff --git a/test/brpc_http_rpc_protocol_unittest.cpp 
b/test/brpc_http_rpc_protocol_unittest.cpp
index f3e07f4a8..333950568 100644
--- a/test/brpc_http_rpc_protocol_unittest.cpp
+++ b/test/brpc_http_rpc_protocol_unittest.cpp
@@ -23,6 +23,7 @@
 #include <cstddef>
 #include <set>
 #include <string>
+#include <thread>
 #include <sys/ioctl.h>
 #include <sys/socket.h>
 #include <butil/build_config.h> // OS_MACOSX
@@ -69,6 +70,9 @@ 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;
+namespace policy {
+DECLARE_int64(h2_max_pending_data_bytes);
+}
 }
 
 int main(int argc, char* argv[]) {
@@ -330,6 +334,21 @@ protected:
     }
 
     void MakeH2EchoResponseBuf(butil::IOBuf* out, int h2_stream_id) {
+        brpc::policy::H2Context* srv_ctx =
+            static_cast<brpc::policy::H2Context*>(_socket->parsing_context());
+        if (srv_ctx == NULL) {
+            srv_ctx = new brpc::policy::H2Context(_socket.get(), &_server);
+            ASSERT_EQ(0, srv_ctx->Init());
+            _socket->initialize_parsing_context(&srv_ctx);
+        }
+        if (srv_ctx->FindStream(h2_stream_id) == NULL) {
+            brpc::policy::H2StreamContext* srv_sctx =
+                new brpc::policy::H2StreamContext(false);
+            srv_sctx->Init(srv_ctx, h2_stream_id);
+            srv_sctx->_remote_ended = true;
+            ASSERT_EQ(0, srv_ctx->TryToInsertStream(h2_stream_id, srv_sctx));
+        }
+
         brpc::Controller cntl;
         test::EchoResponse res;
         res.set_message(EXP_RESPONSE);
@@ -339,7 +358,7 @@ protected:
             EXPECT_TRUE(res.SerializeToZeroCopyStream(&wrapper));
         }
         brpc::policy::H2UnsentResponse* h2_res = 
brpc::policy::H2UnsentResponse::New(&cntl, h2_stream_id, false /*is grpc*/);
-        butil::Status st = h2_res->AppendAndDestroySelf(out, 
_h2_client_sock.get());
+        butil::Status st = h2_res->AppendAndDestroySelf(out, _socket.get());
         ASSERT_TRUE(st.ok());
     }
 
@@ -1714,10 +1733,15 @@ TEST_F(HttpTest, http2_sanity) {
 
     // Check that the first request larger than the default window completes
     // after SETTINGS and WINDOW_UPDATE make more capacity available.
+    // main() sets -socket_max_unwritten_bytes=2000000 for HTTP/1 overcrowding
+    // tests, so raise -h2_max_pending_data_bytes to allow buffering the 2MB
+    // HTTP/2 request before the initial SETTINGS/WINDOW_UPDATE arrives.
+    GFLAGS_NAMESPACE::FlagSaver flag_saver;
+    brpc::policy::FLAGS_h2_max_pending_data_bytes = 4 * 1024 * 1024;
     brpc::Controller cntl;
     test::EchoRequest big_req;
     test::EchoResponse res;
-    std::string message(128 * 1024, 'x');
+    std::string message(2 * 1024 * 1024 /* 2M */, 'x');
     big_req.set_message(message);
     cntl.http_request().set_method(brpc::HTTP_METHOD_POST);
     cntl.http_request().uri() = "/EchoService/Echo";
@@ -1806,6 +1830,33 @@ TEST_F(HttpTest, http2_rst_before_header) {
     ASSERT_TRUE(cntl.Failed());
     ASSERT_TRUE(cntl.ErrorCode() == brpc::EHTTP);
     ASSERT_TRUE(cntl.http_response().status_code() == 
brpc::HTTP_STATUS_INTERNAL_SERVER_ERROR);
+    ASSERT_NE(std::string::npos,
+              cntl.ErrorText().find("terminated by RST_STREAM: 
INTERNAL_ERROR[0x2]"))
+        << cntl.ErrorText();
+}
+
+TEST_F(HttpTest, http2_rst_flow_control_error_message) {
+    brpc::Controller cntl;
+    butil::IOBuf req_out;
+    int h2_stream_id = 0;
+    MakeH2EchoRequestBuf(&req_out, &cntl, &h2_stream_id);
+    butil::IOBuf res_out;
+    char rstbuf[9 /*FRAME_HEAD_SIZE*/ + 4];
+    brpc::policy::SerializeFrameHead(
+        rstbuf, 4, brpc::policy::H2_FRAME_RST_STREAM, 0, h2_stream_id);
+    SaveUint32(rstbuf + 9, brpc::H2_FLOW_CONTROL_ERROR);
+    res_out.append(rstbuf, sizeof(rstbuf));
+    brpc::ParseResult res_pr =
+        brpc::policy::ParseH2Message(&res_out, _h2_client_sock.get(), false, 
NULL);
+    ASSERT_TRUE(res_pr.is_ok());
+    ProcessMessage(brpc::policy::ProcessHttpResponse, res_pr.message(), false);
+    ASSERT_TRUE(cntl.Failed());
+    EXPECT_EQ(brpc::EHTTP, cntl.ErrorCode());
+    EXPECT_EQ(brpc::HTTP_STATUS_INTERNAL_SERVER_ERROR,
+              cntl.http_response().status_code());
+    EXPECT_NE(std::string::npos,
+              cntl.ErrorText().find("terminated by RST_STREAM: 
FLOW_CONTROL_ERROR[0x3]"))
+        << cntl.ErrorText();
 }
 
 TEST_F(HttpTest, http2_rst_after_header_and_data) {
@@ -1867,16 +1918,373 @@ TEST_F(HttpTest, http2_window_used_up_buffers_request) 
{
         butil::Status st = socket_message->AppendAndDestroySelf(&dummy, 
_h2_client_sock.get());
         ASSERT_TRUE(st.ok());
         if (i == nsuc) {
-            ASSERT_GT(ctx->_pending_data_size, 0u);
+            const int blocked_stream_id = h2_req->_stream_id;
+            ASSERT_GT(ctx->_pending_data_size.load(), 0u);
+            ASSERT_NE(nullptr, ctx->FindStream(blocked_stream_id));
             h2_req->DestroyStreamUserData(
                 _h2_client_sock, &cntl, ECANCELED, false);
-            ASSERT_EQ(0u, ctx->_pending_data_size);
+            ASSERT_EQ(0u, ctx->_pending_data_size.load());
+            EXPECT_EQ(nullptr, ctx->FindStream(blocked_stream_id));
         } else {
             h2_req->DestroyStreamUserData(_h2_client_sock, &cntl, 0, false);
         }
     }
 }
 
+TEST_F(HttpTest, 
http2_client_upload_timeout_sends_rst_and_cleans_server_stream) {
+    brpc::Server server;
+    EXPECT_EQ(0, server.AddService(&_svc, brpc::SERVER_DOESNT_OWN_SERVICE));
+    brpc::ServerOptions srv_options;
+    // Configure stream_window_size (256KB) > connection_window_size (64KB) so 
a
+    // 100KB request sends HEADERS + the first ~64KB of DATA frames to the 
server
+    // (stored in server sctx->body(), which is < quota=128KB so server does 
not
+    // immediately send WINDOW_UPDATE), while the remaining ~36KB of DATA stays
+    // buffered in client _pending_data until the RPC times out mid-upload.
+    srv_options.h2_settings.stream_window_size = 256 * 1024;
+    srv_options.h2_settings.connection_window_size =
+        brpc::H2Settings::DEFAULT_INITIAL_WINDOW_SIZE;
+    ASSERT_EQ(0, server.Start(0, &srv_options));
+
+    brpc::Channel channel;
+    brpc::ChannelOptions options;
+    options.protocol = "h2";
+    ASSERT_EQ(0, channel.Init(server.listen_address(), &options));
+
+    // Warmup RPC ensures the connection is established and the client has
+    // received the server's SETTINGS frame.
+    {
+        brpc::Controller cntl;
+        test::EchoRequest req;
+        test::EchoResponse res;
+        req.set_message(EXP_REQUEST);
+        cntl.http_request().set_method(brpc::HTTP_METHOD_POST);
+        cntl.http_request().uri() = "/EchoService/Echo";
+        channel.CallMethod(NULL, &cntl, &req, &res, NULL);
+        ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText();
+        ASSERT_EQ(EXP_RESPONSE, res.message());
+    }
+
+    auto read_bvar = [](const char* name) -> int64_t {
+        std::string val = bvar::Variable::describe_exposed(name);
+        int64_t out = -1;
+        butil::StringToInt64(val, &out);
+        return out;
+    };
+    const int64_t base_stream_count = read_bvar("h2_stream_context_count");
+    const int64_t base_rst_count = read_bvar("h2_stream_reset_count");
+
+    // Case 1: Send a 100KB POST request: ~64KB of DATA is uploaded to the 
server
+    // and ~36KB remains buffered on the client waiting for connection 
WINDOW_UPDATE.
+    // When it times out mid-upload, DestroyStreamUserData must send
+    // RST_STREAM(CANCEL) so the server releases its half-open H2StreamContext
+    // and partial request body.
+    {
+        brpc::Controller cntl;
+        test::EchoRequest req;
+        test::EchoResponse res;
+        req.set_message(std::string(100 * 1024, 'a'));
+        cntl.set_timeout_ms(50);
+        cntl.http_request().set_method(brpc::HTTP_METHOD_POST);
+        cntl.http_request().uri() = "/EchoService/Echo";
+        channel.CallMethod(NULL, &cntl, &req, &res, NULL);
+        ASSERT_TRUE(cntl.Failed());
+        EXPECT_EQ(brpc::ERPCTIMEDOUT, cntl.ErrorCode());
+    }
+
+    for (int i = 0; i < 40; ++i) {
+        if (read_bvar("h2_stream_context_count") == base_stream_count) {
+            break;
+        }
+        bthread_usleep(5000);
+    }
+    EXPECT_EQ(base_stream_count, read_bvar("h2_stream_context_count"));
+    EXPECT_EQ(0, read_bvar("h2_pending_data_bytes"));
+    // Both client (sending RST_STREAM) and server (receiving RST_STREAM in
+    // OnResetStream) increment h2_stream_reset_count in the same process.
+    EXPECT_EQ(base_rst_count + 2, read_bvar("h2_stream_reset_count"));
+
+    // Case 2: Small request completes upload (END_STREAM sent, client 
_pending_data
+    // is empty), then times out while waiting for the server response.
+    // DestroyStreamUserData must still send RST_STREAM(CANCEL).
+    {
+        brpc::Controller cntl;
+        test::EchoRequest req;
+        test::EchoResponse res;
+        req.set_message(EXP_REQUEST);
+        cntl.set_timeout_ms(50);
+        cntl.http_request().set_method(brpc::HTTP_METHOD_POST);
+        cntl.http_request().uri() = "/EchoService/Echo?sleep_ms=200";
+        channel.CallMethod(NULL, &cntl, &req, &res, NULL);
+        ASSERT_TRUE(cntl.Failed());
+        EXPECT_EQ(brpc::ERPCTIMEDOUT, cntl.ErrorCode());
+    }
+    for (int i = 0; i < 40; ++i) {
+        if (read_bvar("h2_stream_context_count") == base_stream_count) {
+            break;
+        }
+        bthread_usleep(5000);
+    }
+    EXPECT_EQ(base_stream_count, read_bvar("h2_stream_context_count"));
+    EXPECT_EQ(base_rst_count + 4, read_bvar("h2_stream_reset_count"));
+}
+
+TEST_F(HttpTest, http2_server_stop_drains_pending_response) {
+    brpc::Server server;
+    brpc::ServerOptions server_options;
+    server_options.idle_timeout_sec = 1;
+    EXPECT_EQ(0, server.AddService(&_svc, brpc::SERVER_DOESNT_OWN_SERVICE));
+    ASSERT_EQ(0, server.Start(0, &server_options));
+
+    butil::fd_guard client_fd(butil::tcp_connect(server.listen_address(), 
NULL));
+    ASSERT_GE(client_fd, 0);
+
+    // Advertise a 10-byte initial stream window so the 19-byte JSON response
+    // ({"message":"world"}) sends 10 bytes immediately and leaves 9 bytes
+    // buffered in the server's H2Context after HttpResponseSender destroys the
+    // server-side Controller.
+    brpc::policy::H2Context* cli_ctx =
+        new brpc::policy::H2Context(_h2_client_sock.get(), NULL);
+    ASSERT_EQ(0, cli_ctx->Init());
+    cli_ctx->_client_preface_pending = true;
+    cli_ctx->_unack_local_settings.stream_window_size = 10;
+    cli_ctx->_local_settings.stream_window_size = 10;
+    _h2_client_sock->initialize_parsing_context(&cli_ctx);
+
+    brpc::Controller cntl;
+    cntl.http_request().uri() = "/EchoService/Echo";
+    butil::IOBuf req_out;
+    int h2_stream_id = 0;
+    MakeH2EchoRequestBuf(&req_out, &cntl, &h2_stream_id);
+    const ssize_t req_size = static_cast<ssize_t>(req_out.size());
+    ASSERT_EQ(req_size, req_out.cut_into_file_descriptor(client_fd));
+
+    auto read_bvar = [](const char* name) -> int64_t {
+        std::string val = bvar::Variable::describe_exposed(name);
+        int64_t out = -1;
+        butil::StringToInt64(val, &out);
+        return out;
+    };
+
+    butil::IOPortal resp_buf;
+    for (int i = 0; i < 100; ++i) {
+        int bytes_in_sock = 0;
+        ioctl(client_fd, FIONREAD, &bytes_in_sock);
+        if (bytes_in_sock > 0) {
+            ASSERT_GT(resp_buf.append_from_file_descriptor(client_fd, 
bytes_in_sock), 0);
+            brpc::ParseResult pr = brpc::policy::ParseH2Message(
+                &resp_buf, _h2_client_sock.get(), false, NULL);
+            ASSERT_EQ(brpc::PARSE_ERROR_NOT_ENOUGH_DATA, pr.error());
+        }
+        brpc::policy::H2StreamContext* cli_sctx = 
cli_ctx->FindStream(h2_stream_id);
+        if (cli_sctx != NULL && cli_sctx->body().size() == 10 &&
+            read_bvar("h2_pending_data_bytes") == 9) {
+            break;
+        }
+        bthread_usleep(5000);
+    }
+    ASSERT_EQ(9, read_bvar("h2_pending_data_bytes"));
+
+    // Stop the server while the remaining 9 bytes of response DATA are waiting
+    // for WINDOW_UPDATE. The connection must stay alive until the deferred
+    // response finishes draining.
+    server.Stop(0);
+    butil::atomic<bool> join_done(false);
+    std::thread join_thread([&]() {
+        server.Join();
+        join_done.store(true, butil::memory_order_release);
+    });
+    bthread_usleep(50000);
+    EXPECT_FALSE(join_done.load(butil::memory_order_acquire));
+    EXPECT_EQ(9, read_bvar("h2_pending_data_bytes"));
+
+    // Send stream and connection WINDOW_UPDATE so the server can flush the
+    // remaining 9 bytes + END_STREAM and finish draining.
+    char winbuf[(brpc::policy::FRAME_HEAD_SIZE + 4) * 2];
+    brpc::policy::SerializeFrameHead(
+        winbuf, 4, brpc::policy::H2_FRAME_WINDOW_UPDATE, 0, h2_stream_id);
+    SaveUint32(winbuf + brpc::policy::FRAME_HEAD_SIZE, 10);
+    brpc::policy::SerializeFrameHead(
+        winbuf + brpc::policy::FRAME_HEAD_SIZE + 4,
+        4, brpc::policy::H2_FRAME_WINDOW_UPDATE, 0, 0);
+    SaveUint32(winbuf + brpc::policy::FRAME_HEAD_SIZE * 2 + 4, 10);
+    ASSERT_EQ(static_cast<ssize_t>(sizeof(winbuf)),
+              ::write(client_fd, winbuf, sizeof(winbuf)));
+
+    join_thread.join();
+    EXPECT_TRUE(join_done.load(butil::memory_order_acquire));
+    EXPECT_EQ(0, read_bvar("h2_pending_data_bytes"));
+
+    brpc::ParseResult res_pr =
+        brpc::MakeParseError(brpc::PARSE_ERROR_NOT_ENOUGH_DATA);
+    for (int i = 0; i < 100 && !res_pr.is_ok(); ++i) {
+        int bytes_in_sock = 0;
+        ioctl(client_fd, FIONREAD, &bytes_in_sock);
+        if (bytes_in_sock > 0) {
+            ASSERT_GT(resp_buf.append_from_file_descriptor(client_fd, 
bytes_in_sock), 0);
+            res_pr = brpc::policy::ParseH2Message(
+                &resp_buf, _h2_client_sock.get(), false, NULL);
+        } else {
+            bthread_usleep(5000);
+        }
+    }
+    ASSERT_TRUE(res_pr.is_ok());
+    ProcessMessage(brpc::policy::ProcessHttpResponse, res_pr.message(), false);
+    ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText();
+    EXPECT_EQ("{\"message\":\"world\"}", 
cntl.response_attachment().to_string());
+}
+
+TEST_F(HttpTest, http2_idle_timeout_cleans_up_pending_response) {
+    brpc::Server server;
+    brpc::ServerOptions server_options;
+    server_options.idle_timeout_sec = 1;
+    EXPECT_EQ(0, server.AddService(&_svc, brpc::SERVER_DOESNT_OWN_SERVICE));
+    ASSERT_EQ(0, server.Start(0, &server_options));
+
+    auto read_bvar = [](const char* name) -> int64_t {
+        std::string val = bvar::Variable::describe_exposed(name);
+        int64_t out = -1;
+        butil::StringToInt64(val, &out);
+        return out;
+    };
+
+    auto start_stalled_zero_window_request = [&](butil::fd_guard* out_fd) {
+        butil::fd_guard fd(butil::tcp_connect(server.listen_address(), NULL));
+        ASSERT_GE(fd, 0);
+
+        brpc::policy::H2Context* cli_ctx =
+            new brpc::policy::H2Context(_h2_client_sock.get(), NULL);
+        ASSERT_EQ(0, cli_ctx->Init());
+        cli_ctx->_client_preface_pending = true;
+        cli_ctx->_unack_local_settings.stream_window_size = 0;
+        cli_ctx->_local_settings.stream_window_size = 0;
+        _h2_client_sock->reset_parsing_context(cli_ctx);
+
+        brpc::Controller cntl;
+        cntl.http_request().uri() = "/EchoService/Echo";
+        butil::IOBuf req_out;
+        int h2_stream_id = 0;
+        MakeH2EchoRequestBuf(&req_out, &cntl, &h2_stream_id);
+        const ssize_t req_size = static_cast<ssize_t>(req_out.size());
+        ASSERT_EQ(req_size, req_out.cut_into_file_descriptor(fd));
+
+        butil::IOPortal resp_buf;
+        for (int i = 0; i < 100; ++i) {
+            int bytes_in_sock = 0;
+            ioctl(fd, FIONREAD, &bytes_in_sock);
+            if (bytes_in_sock > 0) {
+                ASSERT_GT(resp_buf.append_from_file_descriptor(fd, 
bytes_in_sock), 0);
+                brpc::ParseResult pr = brpc::policy::ParseH2Message(
+                    &resp_buf, _h2_client_sock.get(), false, NULL);
+                ASSERT_EQ(brpc::PARSE_ERROR_NOT_ENOUGH_DATA, pr.error());
+            }
+            if (read_bvar("h2_pending_data_bytes") == 19) {
+                break;
+            }
+            bthread_usleep(5000);
+        }
+        ASSERT_EQ(19, read_bvar("h2_pending_data_bytes"));
+        cntl._current_call.stream_user_data->DestroyStreamUserData(
+            _h2_client_sock, &cntl, 0, false);
+        cntl._current_call.stream_user_data = NULL;
+        out_fd->reset(fd.release());
+    };
+
+    // Case 1: While the server is running, idle expiration must fail the
+    // stalled connection and clean up its buffered response even when the peer
+    // keeps TCP open without sending WINDOW_UPDATE or RST_STREAM.
+    butil::fd_guard client_fd1;
+    start_stalled_zero_window_request(&client_fd1);
+    for (int i = 0; i < 300 && read_bvar("h2_pending_data_bytes") != 0; ++i) {
+        bthread_usleep(10000);
+    }
+    ASSERT_EQ(0, read_bvar("h2_pending_data_bytes"));
+
+    // Case 2: If Server::Stop() is called before idle expiration while a
+    // deferred response is buffered on a stalled connection, idle expiration
+    // must still clean up the buffered response and unblock Server::Join().
+    butil::fd_guard client_fd2;
+    start_stalled_zero_window_request(&client_fd2);
+
+    server.Stop(0);
+    butil::atomic<bool> join_done(false);
+    std::thread join_thread([&]() {
+        server.Join();
+        join_done.store(true, butil::memory_order_release);
+    });
+    bthread_usleep(50000);
+    EXPECT_FALSE(join_done.load(butil::memory_order_acquire));
+    EXPECT_EQ(19, read_bvar("h2_pending_data_bytes"));
+
+    for (int i = 0; i < 300 && !join_done.load(butil::memory_order_acquire); 
++i) {
+        bthread_usleep(10000);
+    }
+    EXPECT_TRUE(join_done.load(butil::memory_order_acquire));
+    join_thread.join();
+    EXPECT_EQ(0, read_bvar("h2_pending_data_bytes"));
+    EXPECT_EQ(0, read_bvar("h2_unsent_request_count"));
+}
+
+TEST_F(HttpTest, 
http2_deferred_response_notify_registration_failure_resets_stream) {
+    brpc::Controller cntl;
+    butil::IOBuf req_out;
+    int h2_stream_id = 0;
+    MakeH2EchoRequestBuf(&req_out, &cntl, &h2_stream_id);
+
+    brpc::policy::H2Context* srv_ctx =
+        new brpc::policy::H2Context(_socket.get(), &_server);
+    ASSERT_EQ(0, srv_ctx->Init());
+    _socket->initialize_parsing_context(&srv_ctx);
+    srv_ctx->_remote_settings.stream_window_size = 0;
+
+    brpc::policy::H2StreamContext* srv_sctx =
+        new brpc::policy::H2StreamContext(false);
+    srv_sctx->Init(srv_ctx, h2_stream_id);
+    srv_sctx->_remote_ended = true;
+    ASSERT_EQ(0, srv_ctx->TryToInsertStream(h2_stream_id, srv_sctx));
+
+    // Saturate _socket->_id_wait_list so Socket::NotifyOnFailed() fails with
+    // EAGAIN while _socket itself remains healthy (!_socket->Failed()).
+    bthread_id_t dummy_id = INVALID_BTHREAD_ID;
+    ASSERT_EQ(0, bthread_id_create(&dummy_id, NULL, NULL));
+    {
+        BAIDU_SCOPED_LOCK(_socket->_id_wait_list_mutex);
+        int consecutive_eagain = 0;
+        while (consecutive_eagain < 26000) {
+            const int rc =
+                bthread_id_list_add(&_socket->_id_wait_list, dummy_id);
+            if (rc == EAGAIN) {
+                ++consecutive_eagain;
+            } else {
+                ASSERT_EQ(0, rc);
+                consecutive_eagain = 0;
+            }
+        }
+    }
+    ASSERT_FALSE(_socket->Failed());
+
+    butil::IOBuf res_out;
+    MakeH2EchoResponseBuf(&res_out, h2_stream_id);
+
+    EXPECT_FALSE(_socket->Failed());
+    EXPECT_EQ(nullptr, srv_ctx->FindStream(h2_stream_id));
+    EXPECT_EQ(0u, srv_ctx->_pending_data_size.load());
+    EXPECT_EQ(0, _socket->_idle_fail_ref_count.load());
+
+    brpc::ParseResult res_pr =
+        brpc::policy::ParseH2Message(&res_out, _h2_client_sock.get(), false, 
NULL);
+    ASSERT_TRUE(res_pr.is_ok());
+    ProcessMessage(brpc::policy::ProcessHttpResponse, res_pr.message(), false);
+    ASSERT_TRUE(cntl.Failed());
+    EXPECT_EQ(brpc::EHTTP, cntl.ErrorCode());
+    EXPECT_EQ(brpc::HTTP_STATUS_INTERNAL_SERVER_ERROR,
+              cntl.http_response().status_code());
+    EXPECT_NE(std::string::npos,
+              cntl.ErrorText().find("terminated by RST_STREAM: 
INTERNAL_ERROR[0x2]"))
+        << cntl.ErrorText();
+
+    EXPECT_EQ(0, bthread_id_error(dummy_id, 0));
+}
+
 TEST_F(HttpTest, http2_settings) {
     char settingsbuf[brpc::policy::FRAME_HEAD_SIZE + 36];
     brpc::H2Settings h2_settings;
@@ -3292,6 +3700,9 @@ TEST_F(HttpTest, grpc_auth_failed_response) {
 
     // Initialize the stream context with connection context and stream ID
     h2_msg->Init(h2_ctx, 1); // stream_id = 1
+    h2_msg->_remote_ended = true;
+    h2_msg->AddOneRefForStage2();
+    ASSERT_EQ(0, h2_ctx->TryToInsertStream(1, h2_msg));
 
     // Set socket and arg using existing test pattern
     if (h2_msg->_socket == nullptr) {


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

Reply via email to