private async Task ProcessDataWrites() { // ProcessDataWrites runs for the lifetime of the Http2OutputProducer, and is designed to be reused by multiple streams. // When Http2OutputProducer is no longer used (e.g. a stream is aborted and will no longer be used, or the connection is closed) // it should be disposed so ProcessDataWrites exits. Not disposing won't cause a memory leak in release builds, but in debug // builds active tasks are rooted on Task.s_currentActiveTasks. Dispose could be removed in the future when active tasks are // tracked by a weak reference. See https://github.com/dotnet/runtime/issues/26565 do { FlushResult flushResult = default; ReadResult readResult = default; try { do { var firstWrite = true; readResult = await _pipeReader.ReadAsync(); if (readResult.IsCanceled) { // Response body is aborted, break and complete reader. break; } else if (readResult.IsCompleted && _stream.ResponseTrailers?.Count > 0) { // Output is ending and there are trailers to write // Write any remaining content then write trailers _stream.ResponseTrailers.SetReadOnly(); _stream.DecrementActiveClientStreamCount(); if (readResult.Buffer.Length > 0) { // It is faster to write data and trailers together. Locking once reduces lock contention. flushResult = await _frameWriter.WriteDataAndTrailersAsync(StreamId, _flowControl, readResult.Buffer, firstWrite, _stream.ResponseTrailers); } else { flushResult = await _frameWriter.WriteResponseTrailersAsync(StreamId, _stream.ResponseTrailers); } } else if (readResult.IsCompleted && _streamEnded) { if (readResult.Buffer.Length != 0) { ThrowUnexpectedState(); } // Headers have already been written and there is no other content to write flushResult = await _frameWriter.FlushAsync(outputAborter: null, cancellationToken: default); }