Skip to content

ObjectDisposedException when retrying POST request using AndroidClientHandler #2901

Description

@jNery-zz

ObjectDisposedException when retrying POST request using AndroidClientHandler

Steps to Reproduce

  1. Implement AndroidClientHandler that will retry the same request
public class NativeClientHandler : AndroidClientHandler
{
    bool retry = true;
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        HttpResponseMessage response = await BaseSendAsync(request, cancellationToken);
        if (retry)
        {
            retry = !retry;
            return await SendAsync(request, cancellationToken);
        }
        return response;
    }
    internal Task<HttpResponseMessage> BaseSendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return base.SendAsync(request, cancellationToken);
    }
}

Notice that same implementation above for HttpClientHandler works just fine

public class ManagedClientHandler : HttpClientHandler
{
    bool retry = true;
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        HttpResponseMessage response = await BaseSendAsync(request, cancellationToken);
        if (retry)
        {
            retry = !retry;
            return await SendAsync(request, cancellationToken);
        }
        return response;
    }
    internal Task<HttpResponseMessage> BaseSendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return base.SendAsync(request, cancellationToken);
    }
}
  1. Send POST request that will be re-sent
    Use case: server may respond with JSON error so client can challenge for credential; thus, sending the same POST request with authorization header or parameter.
EventHandler clickedHandler = async (a, b) =>
{
    var dialog = new Android.App.AlertDialog.Builder(this);
    try
    {
        if (a == button)
        {
            var client = new System.Net.Http.HttpClient(new NativeClientHandler());
            var response = await client.PostAsync(new Uri("http://httpbin.org/post"), new System.Net.Http.FormUrlEncodedContent(new[] { new KeyValuePair<string, string>("f", "json") }));
        }
        if (a == button1)
        {
            var client = new System.Net.Http.HttpClient(new ManagedClientHandler());
            var response = await client.PostAsync(new Uri("http://httpbin.org/post"), new System.Net.Http.FormUrlEncodedContent(new[] { new KeyValuePair<string, string>("f", "json") }));
            dialog.SetMessage(await response.Content.ReadAsStringAsync());
        }
        dialog.SetTitle("Success!");
    }
    catch (Exception error)
    {
        Exception ex = error;
        if (ex is AggregateException)
        {
            ex = ((AggregateException)ex).GetBaseException();
        }
        dialog.SetTitle(ex.GetType().Name);
        dialog.SetMessage(ex.Message);
    }
    RunOnUiThread(() => dialog.Show());
};
button.Click += clickedHandler;
button1.Click += clickedHandler

Expected Behavior

Following request should have succeeded.

Actual Behavior

Following request fails with ObjectDisposedException "Cannot access a closed Stream"

Version Information

Microsoft Visual Studio Enterprise 2017
Version 15.9.9
VisualStudio.15.Release/15.9.9+28307.518
Microsoft .NET Framework
Version 4.7.03190

Mono Debugging for Visual Studio 4.13.12-pre (9bc9548)
Support for debugging Mono processes with Visual Studio.

VisualStudio.Mac 1.0
Mac Extension for Visual Studio

Xamarin 4.12.3.80 (d15-9@914127c74)
Visual Studio extension to enable development for Xamarin.iOS and Xamarin.Android.

Xamarin Designer 4.16.13 (45a16efd4)
Visual Studio extension to enable Xamarin Designer tools in Visual Studio.

Xamarin Templates 1.1.128 (6f5ebb2)
Templates for building iOS, Android, and Windows apps with Xamarin and Xamarin.Forms.

Xamarin.Android SDK 9.1.7.0 (HEAD/ba9da7a76)
Xamarin.Android Reference Assemblies and MSBuild support.

Xamarin.iOS and Xamarin.Mac SDK 12.2.1.15 (d60abd1)
Xamarin.iOS and Xamarin.Mac Reference Assemblies and MSBuild support.

Log File

Activity

  1. added this to the d16-2 milestone on Apr 1, 2019
  2. IndexOutOfLimit commented on Jul 8, 2020

    @IndexOutOfLimit

    Any update on this as I am also facing this issue and it needs to be resolved asap on Prod env.

  3. yma44 commented on Sep 22, 2020

    @yma44

    I also started to have the same issue after upgrading.
    from Visual Studio Enterprise 2019 16,2,4 (Xamarin.Andoid 9.4.1.1)
    to Visual Studio Enterprise 2019 16,4.12 (Xamarin.Android 10.1.4.0 (d16-4/e44d1ae) )

    We are doing the similar logic, when the first request failed we retry sending the same request, and now getting the ObjectDisposedException - cannot access closed stream.
    This is the stacktrace.
    at System.IO.StreamHelpers.ValidateCopyToArgs (System.IO.Stream source, System.IO.Stream destination, System.Int32 bufferSize) [0x0003a] in /Users/builder/jenkins/workspace/archive-mono/2020-02/android/release/external/corefx/src/Common/src/CoreLib/System/IO/StreamHelpers.CopyValidation.cs:26
    at System.IO.MemoryStream.CopyToAsync (System.IO.Stream destination, System.Int32 bufferSize, System.Threading.CancellationToken cancellationToken) [0x00000] in /Users/builder/jenkins/workspace/archive-mono/2020-02/android/release/external/corefx/src/Common/src/CoreLib/System/IO/MemoryStream.cs:531
    at Xamarin.Android.Net.AndroidClientHandler.WriteRequestContentToOutput (System.Net.Http.HttpRequestMessage request, Java.Net.HttpURLConnection httpConnection, System.Threading.CancellationToken cancellationToken) [0x00089] in /Users/builder/azdo/_work/204/s/xamarin-android/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.cs:352
    at Xamarin.Android.Net.AndroidClientHandler.DoProcessRequest (System.Net.Http.HttpRequestMessage request, Java.Net.URL javaUrl, Java.Net.HttpURLConnection httpConnection, System.Threading.CancellationToken cancellationToken, Xamarin.Android.Net.AndroidClientHandler+RequestRedirectionState redirectState) [0x002aa] in /Users/builder/azdo/_work/204/s/xamarin-android/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.cs:424
    at Xamarin.Android.Net.AndroidClientHandler.SendAsync (System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) [0x0027d] in /Users/builder/azdo/_work/204/s/xamarin-android/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.cs:286

    It would be great if anyone can tell me when the fix is planned. Thank you!

  4. AdamDiament commented on Jun 29, 2022

    @AdamDiament

    Did anyone get anywhere with this? @jNery-zz ? @yma44 @IndexOutOfLimit

  5. AdamDiament commented on Jun 30, 2022

    @AdamDiament

    Ok I think I have a fix for this:

    @jNery-zz ? @yma44 @IndexOutOfLimit

    Basically, if it's a retry, clone the HttpRequestMessage before resending it, this will reset the stream position to 0. HttpRequestMessages by design can only be used once.

    For your example:

    if (retry)
            {
                retry = !retry;
                request = CloneHttpRequestMessageAsync(request);
                return await SendAsync(request, cancellationToken);
            }
    
       public static async Task<HttpRequestMessage> CloneHttpRequestMessageAsync(HttpRequestMessage req)
            {
                HttpRequestMessage clone = new HttpRequestMessage(req.Method, req.RequestUri);
    
                // Copy the request's content (via a MemoryStream) into the cloned object
                var ms = new MemoryStream();
                if (req.Content != null)
                {
                    await req.Content.CopyToAsync(ms).ConfigureAwait(false);
                    ms.Position = 0;
                    clone.Content = new StreamContent(ms);
    
                    // Copy the content headers
                    if (req.Content.Headers != null)
                        foreach (var h in req.Content.Headers)
                            clone.Content.Headers.Add(h.Key, h.Value);
                }
    
    
                clone.Version = req.Version;
    
                foreach (KeyValuePair<string, object> prop in req.Properties)
                    clone.Properties.Add(prop);
    
                foreach (KeyValuePair<string, IEnumerable<string>> header in req.Headers)
                    clone.Headers.TryAddWithoutValidation(header.Key, header.Value);
    
                return clone;
            }
    
    

    Also as an aside, AndroidClientHandler is now marked as obsolete in the source code with an instruction to use AndroidMessageHandler instead. I've done just that and it works great. It has the same overridable SendAsync method.

  6. dotMorten commented on Oct 3, 2022

    @dotMorten
    Contributor

    FYI similar thing happens on iOS with NSUrlSessionHandler (no error thrown, but it just posts empty content to the server). However SocketsHttpHandler, HttpClientHandler, WebRequestHandler and other .NET ones are not affected and works fine.

    HttpRequestMessages by design can only be used once.

    .NET's HttpHandlers seem to disagree. Also if I created them, why can't I decide when to dispose them? It seems more efficient to be able to reuse your http content, than having to recreate it over and over again for repeated requests.

  7. locked and limited conversation to collaborators on Mar 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Area: HTTPIssues with sockets / HttpClient.

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions