Skip to content

[Mono.Android] Do not dispose request content stream in AndroidMessageHandler #8764

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 23 additions & 24 deletions src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,31 @@ async Task<bool> 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 ()
{
Expand Down