-
Notifications
You must be signed in to change notification settings - Fork 561
Description
Android framework version
net10.0-android (Preview)
Affected platform version
.NET10 RC2
Description
In my app, I have retry logic where I am re-using the same HttpContent if it failed to send in the first attempt. This was made possible by a change from last year: #8764
However, I today discovered a new problem: If the HttpContent is large enough (e.g. 30 MB), an exception might be thrown in this line, halfway through the sending operation - from my experience/logging this often System.IO.IOException: Socket closed or sometimes net_http_request_timedout, 100, but can also happen by just cancelling the provided CancellationToken:
| await stream.CopyToAsync(httpConnection.OutputStream!, 4096, cancellationToken).ConfigureAwait(false); |
In this case, the stream position might have moved on and is no longer 0. And because an exception it thrown, this code to rewind the stream position is not executed:
android/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs
Lines 608 to 609 in 26b6431
| if (stream.CanSeek) | |
| stream.Seek (0, SeekOrigin.Begin); |
Now, when I re-send the request, either the server rejects it because the request is invalid (e.g. when sending a MultipartContent) or the request succeeds, but only parts of the stream are actually uploaded (e.g. when uploading a ByteArrayContent), causing the uploaded file to be corrupted.
In my WinUI 3 app (also using .NET10), there appears to be some logic to rewind the stream position after sending the request (haven't find the code but stream position is at 0 even when I cancel a 30 MB upload after 1 second).
On iOS & macOS, the stream is rewound before it is sent: https://github.com/dotnet/macios/blob/22ededc6faa175101bd68e8f79bd0b915d0c6e52/src/Foundation/NSUrlSessionHandler.cs#L516-L520
I would suggest that this line is being wrapped in a try-catch and rewind the stream position also if an exception occurec, so that the HttpContent can be reused safely
Steps to Reproduce
var tcs = new CancellationTokenSource();
tcs.CancelAfter(1000); // cancel after 1 sec
var client = new HttpClient(new HttpClientHandler { AutomaticDecompression = DecompressionMethods.All });
var byc = new ByteArrayContent(new byte[30_000_000]); // 30 MB of data
var request = new HttpRequestMessage(method, url) { Content = byc };
var stream = await byc.ReadAsStreamAsync();
var position = stream.Position; // 0 - OK
try { await client.SendAsync(request, tcs.Token).ConfigureAwait(false); }
catch (Exception e) // usually TaskCanceledException or System.IO.IOException: Socket closed
{
var stream2 = await byc.ReadAsStreamAsync();
var position2 = stream2.Position; // 30000000 - NOK
stream2.Position = 0; // rewind stream manually
var request2 = new HttpRequestMessage(method, url) { Content = byc };
await client.SendAsync(request2).ConfigureAwait(false); // don't cancel this request
var stream3 = await byc.ReadAsStreamAsync();
var position3 = stream3.Position; // 0 - OK
}
Did you find any workaround?
Manually retrieving the content stream before (re-)sending and setting the stream position to 0