Skip to content

Commit

Permalink
[Mono.Android] Do not dispose request content stream in AndroidMessag…
Browse files Browse the repository at this point in the history
…eHandler (#8764)

Fixes: #2901
Fixes:  #4476
Fixes: #7086

Neither the `SocketsHttpHandler` nor the `iOS/macOS` `NSUrlSessionHandler` dispose the content stream,
let's follow suit.
  • Loading branch information
simonrozsival authored and jonathanpeppers committed Mar 11, 2024
1 parent caf19b0 commit 21ea1d0
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 24 deletions.
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

0 comments on commit 21ea1d0

Please sign in to comment.