diff --git a/src/Http/Http/src/Internal/DefaultHttpResponse.cs b/src/Http/Http/src/Internal/DefaultHttpResponse.cs index 167cd10c40..6caa939444 100644 --- a/src/Http/Http/src/Internal/DefaultHttpResponse.cs +++ b/src/Http/Http/src/Internal/DefaultHttpResponse.cs @@ -70,21 +70,17 @@ namespace Microsoft.AspNetCore.Http set { var otherFeature = _features.Collection.Get(); + if (otherFeature is StreamResponseBodyFeature streamFeature && streamFeature.PriorFeature != null && object.ReferenceEquals(value, streamFeature.PriorFeature.Stream)) { // They're reverting the stream back to the prior one. Revert the whole feature. _features.Collection.Set(streamFeature.PriorFeature); - // CompleteAsync is registered with HttpResponse.OnCompleted and there's no way to unregister it. - // Prevent it from running by marking as disposed. - streamFeature.Dispose(); return; } - var feature = new StreamResponseBodyFeature(value, otherFeature); - OnCompleted(feature.CompleteAsync); - _features.Collection.Set(feature); + _features.Collection.Set(new StreamResponseBodyFeature(value, otherFeature)); } } diff --git a/src/Http/Http/test/Internal/DefaultHttpResponseTests.cs b/src/Http/Http/test/Internal/DefaultHttpResponseTests.cs index fe9b92a908..ea34114879 100644 --- a/src/Http/Http/test/Internal/DefaultHttpResponseTests.cs +++ b/src/Http/Http/test/Internal/DefaultHttpResponseTests.cs @@ -73,6 +73,43 @@ namespace Microsoft.AspNetCore.Http Assert.NotNull(bodyPipe); } + [Fact] + public void ReplacingResponseBody_DoesNotCreateOnCompletedRegistration() + { + var features = new FeatureCollection(); + + var originalStream = new FlushAsyncCheckStream(); + var replacementStream = new FlushAsyncCheckStream(); + + var responseBodyMock = new Mock(); + responseBodyMock.Setup(o => o.Stream).Returns(originalStream); + features.Set(responseBodyMock.Object); + + var responseMock = new Mock(); + features.Set(responseMock.Object); + + var context = new DefaultHttpContext(features); + + Assert.Same(originalStream, context.Response.Body); + Assert.Same(responseBodyMock.Object, context.Features.Get()); + + context.Response.Body = replacementStream; + + Assert.Same(replacementStream, context.Response.Body); + Assert.NotSame(responseBodyMock.Object, context.Features.Get()); + + context.Response.Body = originalStream; + + Assert.Same(originalStream, context.Response.Body); + Assert.Same(responseBodyMock.Object, context.Features.Get()); + + // The real issue was not that an OnCompleted registration existed, but that it would previously flush + // the original response body in the OnCompleted callback after the response body was disposed. + // However, since now there's no longer an OnCompleted registration at all, it's easier to verify that. + // https://github.com/dotnet/aspnetcore/issues/25342 + responseMock.Verify(m => m.OnCompleted(It.IsAny>(), It.IsAny()), Times.Never); + } + [Fact] public async Task ResponseStart_CallsFeatureIfSet() {