From efcd007b0ce2e5a69f684dcf747f90d1a6531ef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Rozs=C3=ADval?= Date: Tue, 27 Feb 2024 11:33:34 +0100 Subject: [PATCH] [Mono.Android] Do not dispose request content stream in AndroidMessageHandler (#8764) Fixes: https://github.com/xamarin/xamarin-android/issues/2901 Fixes: https://github.com/xamarin/xamarin-android/issues/4476 Fixes: https://github.com/xamarin/xamarin-android/issues/7086 Neither the `SocketsHttpHandler` nor the `iOS/macOS` `NSUrlSessionHandler` dispose the content stream, let's follow suit. --- .../AndroidMessageHandler.cs | 47 +++++++++---------- .../AndroidMessageHandlerTests.cs | 25 ++++++++++ 2 files changed, 48 insertions(+), 24 deletions(-) diff --git a/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs b/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs index 44883640487..26645e6fdcb 100644 --- a/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs +++ b/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs @@ -536,30 +536,29 @@ protected virtual async Task WriteRequestContentToOutput (HttpRequestMessage req if (request.Content is null) return; - using (var stream = await request.Content.ReadAsStreamAsync ().ConfigureAwait (false)) { - await stream.CopyToAsync(httpConnection.OutputStream!, 4096, cancellationToken).ConfigureAwait(false); - - // - // Rewind the stream to beginning in case the HttpContent implementation - // will be accessed again (e.g. after redirect) and it keeps its stream - // open behind the scenes instead of recreating it on the next call to - // ReadAsStreamAsync. If we don't rewind it, the ReadAsStreamAsync - // call above will throw an exception as we'd be attempting to read an - // already "closed" stream (that is one whose Position is set to its - // end). - // - // This is not a perfect solution since the HttpContent may do weird - // things in its implementation, but it's better than copying the - // content into a buffer since we have no way of knowing how the data is - // read or generated and also we don't want to keep potentially large - // amounts of data in memory (which would happen if we read the content - // into a byte[] buffer and kept it cached for re-use on redirect). - // - // See https://bugzilla.xamarin.com/show_bug.cgi?id=55477 - // - if (stream.CanSeek) - stream.Seek (0, SeekOrigin.Begin); - } + var stream = await request.Content.ReadAsStreamAsync ().ConfigureAwait (false); + await stream.CopyToAsync(httpConnection.OutputStream!, 4096, cancellationToken).ConfigureAwait(false); + + // + // Rewind the stream to beginning in case the HttpContent implementation + // will be accessed again (e.g. after redirect) and it keeps its stream + // open behind the scenes instead of recreating it on the next call to + // ReadAsStreamAsync. If we don't rewind it, the ReadAsStreamAsync + // call above will throw an exception as we'd be attempting to read an + // already "closed" stream (that is one whose Position is set to its + // end). + // + // This is not a perfect solution since the HttpContent may do weird + // things in its implementation, but it's better than copying the + // content into a buffer since we have no way of knowing how the data is + // read or generated and also we don't want to keep potentially large + // amounts of data in memory (which would happen if we read the content + // into a byte[] buffer and kept it cached for re-use on redirect). + // + // See https://bugzilla.xamarin.com/show_bug.cgi?id=55477 + // + if (stream.CanSeek) + stream.Seek (0, SeekOrigin.Begin); } internal Task WriteRequestContentToOutputInternal (HttpRequestMessage request, HttpURLConnection httpConnection, CancellationToken cancellationToken) diff --git a/tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerTests.cs b/tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerTests.cs index 96f66778ef3..b71ee2893da 100644 --- a/tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerTests.cs +++ b/tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerTests.cs @@ -98,6 +98,31 @@ async Task DoDecompression (string urlPath, string encoding, string jsonFi return true; } + [Test] + public async Task DoesNotDisposeContentStream() + { + using var listener = new HttpListener (); + listener.Prefixes.Add ("http://+:47663/"); + listener.Start (); + listener.BeginGetContext (ar => { + var ctx = listener.EndGetContext (ar); + ctx.Response.StatusCode = 204; + ctx.Response.ContentLength64 = 0; + ctx.Response.Close (); + }, null); + + var jsonContent = new StringContent ("hello"); + var request = new HttpRequestMessage (HttpMethod.Post, "http://localhost:47663/") { Content = jsonContent }; + + var response = await new HttpClient (new AndroidMessageHandler ()).SendAsync (request); + Assert.True (response.IsSuccessStatusCode); + + var contentValue = await jsonContent.ReadAsStringAsync (); + Assert.AreEqual ("hello", contentValue); + + listener.Close (); + } + [Test] public async Task ServerCertificateCustomValidationCallback_ApproveRequest () {