From 7ae11f5dc066ce99c6821db28e32b33dbd3fb34f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 12:56:59 +0000 Subject: [PATCH 1/5] Make LimitArrayPoolWriteStream.Dispose throw; call ReturnAllPooledBuffers directly Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/67c929c4-11bf-4536-bd71-7f640ddcfb01 Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com> --- .../src/System/Net/Http/HttpClient.cs | 44 ++++++++++++------- .../src/System/Net/Http/HttpContent.cs | 23 +++++++--- .../tests/UnitTests/StreamToStreamCopyTest.cs | 17 ++++--- 3 files changed, 58 insertions(+), 26 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs index 3d59884e4ea53e..3aaa0097515139 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs @@ -199,23 +199,30 @@ private async Task GetStringAsyncCore(HttpRequestMessage request, Cancel // Since the underlying byte[] will never be exposed, we use an ArrayPool-backed // stream to which we copy all of the data from the response. - using var buffer = new HttpContent.LimitArrayPoolWriteStream( + var buffer = new HttpContent.LimitArrayPoolWriteStream( _maxResponseContentBufferSize, c.Headers.ContentLength.GetValueOrDefault(), getFinalSizeFromPool: true); - using Stream responseStream = c.TryReadAsStream() ?? await c.ReadAsStreamAsync(cts.Token).ConfigureAwait(false); try { - await responseStream.CopyToAsync(buffer, cts.Token).ConfigureAwait(false); + using Stream responseStream = c.TryReadAsStream() ?? await c.ReadAsStreamAsync(cts.Token).ConfigureAwait(false); + try + { + await responseStream.CopyToAsync(buffer, cts.Token).ConfigureAwait(false); + } + catch (Exception e) when (HttpContent.StreamCopyExceptionNeedsWrapping(e)) + { + throw HttpContent.WrapStreamCopyException(e); + } + + // Decode and return the data from the buffer. + return HttpContent.ReadBufferAsString(buffer, c.Headers); } - catch (Exception e) when (HttpContent.StreamCopyExceptionNeedsWrapping(e)) + finally { - throw HttpContent.WrapStreamCopyException(e); + buffer.ReturnAllPooledBuffers(); } - - // Decode and return the data from the buffer. - return HttpContent.ReadBufferAsString(buffer, c.Headers); } catch (Exception e) { @@ -275,22 +282,29 @@ private async Task GetByteArrayAsyncCore(HttpRequestMessage request, Can // the buffer potentially several times and that it's unlikely the underlying buffer // at the end will be the exact size needed, in which case it's more beneficial to use // ArrayPool buffers and copy out to a new array at the end. - using var buffer = new HttpContent.LimitArrayPoolWriteStream( + var buffer = new HttpContent.LimitArrayPoolWriteStream( _maxResponseContentBufferSize, c.Headers.ContentLength.GetValueOrDefault(), getFinalSizeFromPool: false); - using Stream responseStream = c.TryReadAsStream() ?? await c.ReadAsStreamAsync(cts.Token).ConfigureAwait(false); try { - await responseStream.CopyToAsync(buffer, cts.Token).ConfigureAwait(false); + using Stream responseStream = c.TryReadAsStream() ?? await c.ReadAsStreamAsync(cts.Token).ConfigureAwait(false); + try + { + await responseStream.CopyToAsync(buffer, cts.Token).ConfigureAwait(false); + } + catch (Exception e) when (HttpContent.StreamCopyExceptionNeedsWrapping(e)) + { + throw HttpContent.WrapStreamCopyException(e); + } + + return buffer.ToArray(); } - catch (Exception e) when (HttpContent.StreamCopyExceptionNeedsWrapping(e)) + finally { - throw HttpContent.WrapStreamCopyException(e); + buffer.ReturnAllPooledBuffers(); } - - return buffer.ToArray(); } catch (Exception e) { diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs index 2ee82b8298b82b..b0951318c2b542 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs @@ -412,7 +412,7 @@ internal void LoadIntoBuffer(long maxBufferSize, CancellationToken cancellationT } catch (Exception e) { - tempBuffer.Dispose(); + tempBuffer.ReturnAllPooledBuffers(); if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, e); @@ -499,7 +499,7 @@ public Task LoadIntoBufferAsync(long maxBufferSize, CancellationToken cancellati } catch (Exception e) { - tempBuffer.Dispose(); + tempBuffer.ReturnAllPooledBuffers(); if (StreamCopyExceptionNeedsWrapping(e)) { @@ -519,7 +519,7 @@ private async Task LoadIntoBufferAsyncCore(Task serializeToStreamTask, LimitArra } catch (Exception e) { - tempBuffer.Dispose(); // Cleanup partially filled stream. + tempBuffer.ReturnAllPooledBuffers(); // Cleanup partially filled stream. Exception we = GetStreamCopyException(e); if (we != e) throw we; throw; @@ -653,7 +653,7 @@ protected virtual void Dispose(bool disposing) if (IsBuffered) { - _bufferedContent.Dispose(); + _bufferedContent.ReturnAllPooledBuffers(); } } } @@ -843,7 +843,18 @@ public LimitArrayPoolWriteStream(int maxBufferSize, long expectedFinalSize, bool protected override void Dispose(bool disposing) { - ReturnAllPooledBuffers(); + // User code must never dispose this stream. It is an internal implementation detail + // exposed to user-provided HttpContent.SerializeToStream(Async) overrides, and the + // lifetime of the underlying pooled buffers is owned by HttpContent, not the user. + // Returning those buffers more than once would corrupt ArrayPool.Shared process-wide + // and also break other HttpContent operations (e.g. ReadAsByteArrayAsync, ReadBufferAsString, + // ReadAsStreamAsync) which assume the stream is still populated. + // All internal cleanup goes through ReturnAllPooledBuffers directly. + if (disposing) + { + throw new NotSupportedException(); + } + base.Dispose(disposing); } @@ -1075,7 +1086,7 @@ public void CopyToCore(Span destination) _lastBuffer.AsSpan(0, _lastBufferOffset).CopyTo(destination); } - private void ReturnAllPooledBuffers() + internal void ReturnAllPooledBuffers() { if (_pooledBuffers is byte[]?[] buffers) { diff --git a/src/libraries/System.Net.Http/tests/UnitTests/StreamToStreamCopyTest.cs b/src/libraries/System.Net.Http/tests/UnitTests/StreamToStreamCopyTest.cs index 637b3381877136..2942524eac8015 100644 --- a/src/libraries/System.Net.Http/tests/UnitTests/StreamToStreamCopyTest.cs +++ b/src/libraries/System.Net.Http/tests/UnitTests/StreamToStreamCopyTest.cs @@ -107,7 +107,7 @@ public async Task MemoryStream_To_LimitMemoryStream_NoCapacity(bool sourceIsExpo { byte[] input = CreateByteArray(8192); MemoryStream source = CreateSourceMemoryStream(sourceIsExposable, input); - using var destination = new HttpContent.LimitArrayPoolWriteStream(int.MaxValue, 0, getFinalSizeFromPool: false); + var destination = new HttpContent.LimitArrayPoolWriteStream(int.MaxValue, 0, getFinalSizeFromPool: false); await StreamToStreamCopy.CopyAsync(source, destination, 4096, disposeSource); @@ -128,7 +128,7 @@ public async Task MemoryStream_To_LimitMemoryStream_EqualCapacity(bool sourceIsE { byte[] input = CreateByteArray(8192); MemoryStream source = CreateSourceMemoryStream(sourceIsExposable, input); - using var destination = new HttpContent.LimitArrayPoolWriteStream(int.MaxValue, input.Length, getFinalSizeFromPool); + var destination = new HttpContent.LimitArrayPoolWriteStream(int.MaxValue, input.Length, getFinalSizeFromPool); await StreamToStreamCopy.CopyAsync(source, destination, 4096, disposeSource); @@ -292,13 +292,20 @@ public void LimitMemoryStream_ResizingLogicWorks(bool getFinalSizeFromPool) } else { - using var smallDestination = new HttpContent.LimitArrayPoolWriteStream(maxBufferSize: actualSize - 1, expectedSize, getFinalSizeFromPool); - capacityEx = Assert.Throws(() => WriteChunks(smallDestination, actualSize)); + var smallDestination = new HttpContent.LimitArrayPoolWriteStream(maxBufferSize: actualSize - 1, expectedSize, getFinalSizeFromPool); + try + { + capacityEx = Assert.Throws(() => WriteChunks(smallDestination, actualSize)); + } + finally + { + smallDestination.ReturnAllPooledBuffers(); + } } Assert.Equal(HttpRequestError.ConfigurationLimitExceeded, capacityEx.HttpRequestError); - using var destination = new HttpContent.LimitArrayPoolWriteStream(maxBufferSize: actualSize + 42, expectedSize, getFinalSizeFromPool); + var destination = new HttpContent.LimitArrayPoolWriteStream(maxBufferSize: actualSize + 42, expectedSize, getFinalSizeFromPool); WriteChunks(destination, actualSize); if (!getFinalSizeFromPool && expectedSize == actualSize) From 139daeb78e875454eae32c03368b14d0d32e90eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 13:14:31 +0000 Subject: [PATCH 2/5] Address code review: add exception message; wrap test destination in try/finally Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/67c929c4-11bf-4536-bd71-7f640ddcfb01 Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com> --- .../src/System/Net/Http/HttpContent.cs | 2 +- .../tests/UnitTests/StreamToStreamCopyTest.cs | 43 +++++++++++-------- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs index b0951318c2b542..0cb5c35a06f05c 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs @@ -852,7 +852,7 @@ protected override void Dispose(bool disposing) // All internal cleanup goes through ReturnAllPooledBuffers directly. if (disposing) { - throw new NotSupportedException(); + throw new NotSupportedException("This stream is owned by HttpContent and must not be disposed by user code; doing so would corrupt ArrayPool.Shared and break other HttpContent operations."); } base.Dispose(disposing); diff --git a/src/libraries/System.Net.Http/tests/UnitTests/StreamToStreamCopyTest.cs b/src/libraries/System.Net.Http/tests/UnitTests/StreamToStreamCopyTest.cs index 2942524eac8015..41ffb5e4f6cf7c 100644 --- a/src/libraries/System.Net.Http/tests/UnitTests/StreamToStreamCopyTest.cs +++ b/src/libraries/System.Net.Http/tests/UnitTests/StreamToStreamCopyTest.cs @@ -306,29 +306,36 @@ public void LimitMemoryStream_ResizingLogicWorks(bool getFinalSizeFromPool) Assert.Equal(HttpRequestError.ConfigurationLimitExceeded, capacityEx.HttpRequestError); var destination = new HttpContent.LimitArrayPoolWriteStream(maxBufferSize: actualSize + 42, expectedSize, getFinalSizeFromPool); - WriteChunks(destination, actualSize); - - if (!getFinalSizeFromPool && expectedSize == actualSize) - { - Assert.Equal(currentInput, destination.GetFirstBuffer()); - Assert.Equal(actualSize, destination.GetSingleBuffer().Length); - Assert.Same(destination.ToArray(), destination.GetSingleBuffer()); - } - else + try { - Assert.True(currentInput.StartsWith(destination.GetFirstBuffer())); - } + WriteChunks(destination, actualSize); + + if (!getFinalSizeFromPool && expectedSize == actualSize) + { + Assert.Equal(currentInput, destination.GetFirstBuffer()); + Assert.Equal(actualSize, destination.GetSingleBuffer().Length); + Assert.Same(destination.ToArray(), destination.GetSingleBuffer()); + } + else + { + Assert.True(currentInput.StartsWith(destination.GetFirstBuffer())); + } - destination.ReallocateIfPooled(); - Assert.Equal(currentInput, destination.CreateCopy()); + destination.ReallocateIfPooled(); + Assert.Equal(currentInput, destination.CreateCopy()); - if (getFinalSizeFromPool || actualSize == expectedSize) - { - Assert.Equal(actualSize, destination.GetSingleBuffer().Length); + if (getFinalSizeFromPool || actualSize == expectedSize) + { + Assert.Equal(actualSize, destination.GetSingleBuffer().Length); + } + else + { + Assert.True(actualSize <= destination.GetSingleBuffer().Length); + } } - else + finally { - Assert.True(actualSize <= destination.GetSingleBuffer().Length); + destination.ReturnAllPooledBuffers(); } } } From 1239c1bbb47bf44037828b9b1eb3931721f26f21 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 13:37:28 +0000 Subject: [PATCH 3/5] Merge buffer-cleanup into outer finally; Dispose throws InvalidOperationException Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/648b000d-d351-4916-bf8c-13142b1ebff1 Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com> --- .../src/System/Net/Http/HttpClient.cs | 48 ++++++++----------- .../src/System/Net/Http/HttpContent.cs | 7 +-- 2 files changed, 20 insertions(+), 35 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs index 3aaa0097515139..d7758c61b687ce 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs @@ -182,6 +182,7 @@ private async Task GetStringAsyncCore(HttpRequestMessage request, Cancel (CancellationTokenSource cts, bool disposeCts, CancellationTokenSource pendingRequestsCts) = PrepareCancellationTokenSource(cancellationToken); HttpResponseMessage? response = null; + HttpContent.LimitArrayPoolWriteStream? buffer = null; try { // Wait for the response message and make sure it completed successfully. @@ -199,30 +200,23 @@ private async Task GetStringAsyncCore(HttpRequestMessage request, Cancel // Since the underlying byte[] will never be exposed, we use an ArrayPool-backed // stream to which we copy all of the data from the response. - var buffer = new HttpContent.LimitArrayPoolWriteStream( + buffer = new HttpContent.LimitArrayPoolWriteStream( _maxResponseContentBufferSize, c.Headers.ContentLength.GetValueOrDefault(), getFinalSizeFromPool: true); + using Stream responseStream = c.TryReadAsStream() ?? await c.ReadAsStreamAsync(cts.Token).ConfigureAwait(false); try { - using Stream responseStream = c.TryReadAsStream() ?? await c.ReadAsStreamAsync(cts.Token).ConfigureAwait(false); - try - { - await responseStream.CopyToAsync(buffer, cts.Token).ConfigureAwait(false); - } - catch (Exception e) when (HttpContent.StreamCopyExceptionNeedsWrapping(e)) - { - throw HttpContent.WrapStreamCopyException(e); - } - - // Decode and return the data from the buffer. - return HttpContent.ReadBufferAsString(buffer, c.Headers); + await responseStream.CopyToAsync(buffer, cts.Token).ConfigureAwait(false); } - finally + catch (Exception e) when (HttpContent.StreamCopyExceptionNeedsWrapping(e)) { - buffer.ReturnAllPooledBuffers(); + throw HttpContent.WrapStreamCopyException(e); } + + // Decode and return the data from the buffer. + return HttpContent.ReadBufferAsString(buffer, c.Headers); } catch (Exception e) { @@ -231,6 +225,7 @@ private async Task GetStringAsyncCore(HttpRequestMessage request, Cancel } finally { + buffer?.ReturnAllPooledBuffers(); FinishSend(response, cts, disposeCts, telemetryStarted, responseContentTelemetryStarted); } } @@ -261,6 +256,7 @@ private async Task GetByteArrayAsyncCore(HttpRequestMessage request, Can (CancellationTokenSource cts, bool disposeCts, CancellationTokenSource pendingRequestsCts) = PrepareCancellationTokenSource(cancellationToken); HttpResponseMessage? response = null; + HttpContent.LimitArrayPoolWriteStream? buffer = null; try { // Wait for the response message and make sure it completed successfully. @@ -282,29 +278,22 @@ private async Task GetByteArrayAsyncCore(HttpRequestMessage request, Can // the buffer potentially several times and that it's unlikely the underlying buffer // at the end will be the exact size needed, in which case it's more beneficial to use // ArrayPool buffers and copy out to a new array at the end. - var buffer = new HttpContent.LimitArrayPoolWriteStream( + buffer = new HttpContent.LimitArrayPoolWriteStream( _maxResponseContentBufferSize, c.Headers.ContentLength.GetValueOrDefault(), getFinalSizeFromPool: false); + using Stream responseStream = c.TryReadAsStream() ?? await c.ReadAsStreamAsync(cts.Token).ConfigureAwait(false); try { - using Stream responseStream = c.TryReadAsStream() ?? await c.ReadAsStreamAsync(cts.Token).ConfigureAwait(false); - try - { - await responseStream.CopyToAsync(buffer, cts.Token).ConfigureAwait(false); - } - catch (Exception e) when (HttpContent.StreamCopyExceptionNeedsWrapping(e)) - { - throw HttpContent.WrapStreamCopyException(e); - } - - return buffer.ToArray(); + await responseStream.CopyToAsync(buffer, cts.Token).ConfigureAwait(false); } - finally + catch (Exception e) when (HttpContent.StreamCopyExceptionNeedsWrapping(e)) { - buffer.ReturnAllPooledBuffers(); + throw HttpContent.WrapStreamCopyException(e); } + + return buffer.ToArray(); } catch (Exception e) { @@ -313,6 +302,7 @@ private async Task GetByteArrayAsyncCore(HttpRequestMessage request, Can } finally { + buffer?.ReturnAllPooledBuffers(); FinishSend(response, cts, disposeCts, telemetryStarted, responseContentTelemetryStarted); } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs index 0cb5c35a06f05c..0052f8d9afa09c 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs @@ -850,12 +850,7 @@ protected override void Dispose(bool disposing) // and also break other HttpContent operations (e.g. ReadAsByteArrayAsync, ReadBufferAsString, // ReadAsStreamAsync) which assume the stream is still populated. // All internal cleanup goes through ReturnAllPooledBuffers directly. - if (disposing) - { - throw new NotSupportedException("This stream is owned by HttpContent and must not be disposed by user code; doing so would corrupt ArrayPool.Shared and break other HttpContent operations."); - } - - base.Dispose(disposing); + throw new InvalidOperationException(); } /// Should only be called once. From 841446af3fed968689bd892a2802aa41fc94e502 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 13:55:05 +0000 Subject: [PATCH 4/5] Address review: trim comment, simplify test buffer cleanup Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/419e5e57-fc2e-45ea-94ed-3a3b584e22dd Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com> --- .../src/System/Net/Http/HttpContent.cs | 3 - .../tests/UnitTests/StreamToStreamCopyTest.cs | 59 ++++++++----------- 2 files changed, 24 insertions(+), 38 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs index 0052f8d9afa09c..4d5dc46c4342f9 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs @@ -846,9 +846,6 @@ protected override void Dispose(bool disposing) // User code must never dispose this stream. It is an internal implementation detail // exposed to user-provided HttpContent.SerializeToStream(Async) overrides, and the // lifetime of the underlying pooled buffers is owned by HttpContent, not the user. - // Returning those buffers more than once would corrupt ArrayPool.Shared process-wide - // and also break other HttpContent operations (e.g. ReadAsByteArrayAsync, ReadBufferAsString, - // ReadAsStreamAsync) which assume the stream is still populated. // All internal cleanup goes through ReturnAllPooledBuffers directly. throw new InvalidOperationException(); } diff --git a/src/libraries/System.Net.Http/tests/UnitTests/StreamToStreamCopyTest.cs b/src/libraries/System.Net.Http/tests/UnitTests/StreamToStreamCopyTest.cs index 41ffb5e4f6cf7c..1ca76d929d717a 100644 --- a/src/libraries/System.Net.Http/tests/UnitTests/StreamToStreamCopyTest.cs +++ b/src/libraries/System.Net.Http/tests/UnitTests/StreamToStreamCopyTest.cs @@ -293,50 +293,39 @@ public void LimitMemoryStream_ResizingLogicWorks(bool getFinalSizeFromPool) else { var smallDestination = new HttpContent.LimitArrayPoolWriteStream(maxBufferSize: actualSize - 1, expectedSize, getFinalSizeFromPool); - try - { - capacityEx = Assert.Throws(() => WriteChunks(smallDestination, actualSize)); - } - finally - { - smallDestination.ReturnAllPooledBuffers(); - } + capacityEx = Assert.Throws(() => WriteChunks(smallDestination, actualSize)); + smallDestination.ReturnAllPooledBuffers(); } Assert.Equal(HttpRequestError.ConfigurationLimitExceeded, capacityEx.HttpRequestError); var destination = new HttpContent.LimitArrayPoolWriteStream(maxBufferSize: actualSize + 42, expectedSize, getFinalSizeFromPool); - try + WriteChunks(destination, actualSize); + + if (!getFinalSizeFromPool && expectedSize == actualSize) + { + Assert.Equal(currentInput, destination.GetFirstBuffer()); + Assert.Equal(actualSize, destination.GetSingleBuffer().Length); + Assert.Same(destination.ToArray(), destination.GetSingleBuffer()); + } + else + { + Assert.True(currentInput.StartsWith(destination.GetFirstBuffer())); + } + + destination.ReallocateIfPooled(); + Assert.Equal(currentInput, destination.CreateCopy()); + + if (getFinalSizeFromPool || actualSize == expectedSize) { - WriteChunks(destination, actualSize); - - if (!getFinalSizeFromPool && expectedSize == actualSize) - { - Assert.Equal(currentInput, destination.GetFirstBuffer()); - Assert.Equal(actualSize, destination.GetSingleBuffer().Length); - Assert.Same(destination.ToArray(), destination.GetSingleBuffer()); - } - else - { - Assert.True(currentInput.StartsWith(destination.GetFirstBuffer())); - } - - destination.ReallocateIfPooled(); - Assert.Equal(currentInput, destination.CreateCopy()); - - if (getFinalSizeFromPool || actualSize == expectedSize) - { - Assert.Equal(actualSize, destination.GetSingleBuffer().Length); - } - else - { - Assert.True(actualSize <= destination.GetSingleBuffer().Length); - } + Assert.Equal(actualSize, destination.GetSingleBuffer().Length); } - finally + else { - destination.ReturnAllPooledBuffers(); + Assert.True(actualSize <= destination.GetSingleBuffer().Length); } + + destination.ReturnAllPooledBuffers(); } } From 755c1a0de0ad4ae1d0dc2a81a0bef7bcc3de269c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 14:20:43 +0000 Subject: [PATCH 5/5] Add test triggering InvalidOperationException on disposing LimitArrayPoolWriteStream Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/2bc5c1de-f6bd-40ff-9f88-efbc27d10f7e Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com> --- .../tests/UnitTests/HttpContentTest.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/libraries/System.Net.Http/tests/UnitTests/HttpContentTest.cs b/src/libraries/System.Net.Http/tests/UnitTests/HttpContentTest.cs index 61f3c5c5fcea43..3f8772bab0a162 100644 --- a/src/libraries/System.Net.Http/tests/UnitTests/HttpContentTest.cs +++ b/src/libraries/System.Net.Http/tests/UnitTests/HttpContentTest.cs @@ -32,6 +32,28 @@ public async Task Dispose_BufferContentThenDisposeContent_BufferedStreamGetsDisp Assert.Null(bufferedStream.GetSingleBuffer()); } + [Fact] + public async Task SerializeToStreamAsync_UserDisposesBufferedStream_Throws() + { + var content = new DisposeBufferedStreamContent(); + await Assert.ThrowsAsync(() => content.LoadIntoBufferAsync()); + } + + private sealed class DisposeBufferedStreamContent : HttpContent + { + protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) + { + stream.Dispose(); + return Task.CompletedTask; + } + + protected internal override bool TryComputeLength(out long length) + { + length = 0; + return false; + } + } + [Theory] [InlineData(1, 100, 99, 1)] [InlineData(1, 100, 50, 99)]