diff --git a/internal/transport/http2_server.go b/internal/transport/http2_server.go index d8936fd8..d038b2df 100644 --- a/internal/transport/http2_server.go +++ b/internal/transport/http2_server.go @@ -1030,6 +1030,9 @@ func (t *http2Server) deleteStream(s *Stream, eosReceived bool) { // closeStream clears the footprint of a stream when the stream is not needed // any more. func (t *http2Server) closeStream(s *Stream, rst bool, rstCode http2.ErrCode, hdr *headerFrame, eosReceived bool) { + // Mark the stream as done + oldState := s.swapState(streamDone) + // In case stream sending and receiving are invoked in separate // goroutines (e.g., bi-directional streaming), cancel needs to be // called to interrupt the potential blocking on other goroutines. @@ -1051,8 +1054,19 @@ func (t *http2Server) closeStream(s *Stream, rst bool, rstCode http2.ErrCode, hd return } + // We do the check here, because of the following scenario: + // 1. closeStream is called first with a trailer. A trailer item with a piggybacked cleanup item + // is put to control buffer. + // 2. Loopy writer is waiting on a stream quota. It will never get it because client errored at + // some point. So loopy can't act on trailer + // 3. Client sends a RST_STREAM due to the error. Then closeStream is called without a trailer as + // the result of the received RST_STREAM. + // If we do this check at the beginning of the closeStream, then we won't put a cleanup item in + // response to received RST_STREAM into the control buffer and outStream in loopy writer will + // never get cleaned up. + // If the stream is already done, don't send the trailer. - if s.swapState(streamDone) == streamDone { + if oldState == streamDone { return }