dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.95k stars 4.65k forks source link

HttpClient.Send not supported in MAUI running on Android or iOS causing PlatformNotSupportedException #104716

Open zijianhuang opened 2 months ago

zijianhuang commented 2 months ago

Description

HttpClient.Send is not supported in a MAUI app running on Android or iOS, though on Windows, it is OK.

Reproduction Steps

        public DemoWebApi.Controllers.Client.Hero[] GetHeroes(Action<System.Net.Http.Headers.HttpRequestHeaders> handleHeaders = null)
        {
            var requestUri = "api/Heroes";
            using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, requestUri);
            handleHeaders?.Invoke(httpRequestMessage.Headers);
            var responseMessage = client.Send(httpRequestMessage);
            try
            {
                responseMessage.EnsureSuccessStatusCodeEx();
                if (responseMessage.StatusCode == System.Net.HttpStatusCode.NoContent) { return null; }
                var stream = responseMessage.Content.ReadAsStream();
                return JsonSerializer.Deserialize<DemoWebApi.Controllers.Client.Hero[]>(stream, jsonSerializerSettings);
            }
            finally
            {
                responseMessage.Dispose();
            }
        }

Checkout branch: https://github.com/zijianhuang/DemoCoreWeb/tree/HttpClientSend_Bug And run one of the MAUI mobile app:

Expected behavior

Expected the codes work like "HttpClient.SendAsync(...).Result".

Actual behavior

Error:

System.PlatformNotSupportedException: 'Operation is not supported on this platform.'

When running client.Send(httpRequestMessage)

Regression?

Related to https://github.com/dotnet/maui/issues/10470

Known Workarounds

client.SendAsync(httpRequestMessage).Result;

Configuration

latest .NET 8 as of 2024-07-11 latest MAUI dependencies

Other information

And in https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs,

 [UnsupportedOSPlatform("browser")]
        public HttpResponseMessage Send(HttpRequestMessage request) =>
            Send(request, DefaultCompletionOption, cancellationToken: default);

Respective prototypes should have got Android and iOS included in UnsupportedOSPlatform attributes. And it will be even better that the implementation of Send is supported in Android and iOS.

dotnet-policy-service[bot] commented 2 months ago

Tagging subscribers to this area: @dotnet/ncl See info in area-owners.md if you want to be subscribed.

ManickaP commented 2 months ago

@zijianhuang please be aware, that we strongly discourage using the sync overload in HttpClient. Only if you're already in sync-context, which is outside of your control, this should be used. It has also a lot of limitations, it works only with HTTP 1.1, it's not completely sync if a new connection is getting established etc. @simonrozsival I assume you don't plan to introduce sync overload for mobile platforms, do you?

So this issue is about properly annotating the methods with UnsupportedOS attribute.

simonrozsival commented 2 months ago

@ManickaP correct, there are no such plans

zijianhuang commented 2 months ago

@ManickaP , agree overall. In my application codes, I mostly used SendAsync() in service brokers and apps. And for desktop and mobile apps, in the context of some built-in event functions that provide no async function prototypes, I have been using SendAsync().Result, because HttpClient.Send() was introduced in .NET 5 (not available in .NET Framework and Standard), which I have recently found out, and tried to use Send() to replace SendAsync().Result.

@simonrozsival , I am a bit curious, what was the purpose to introduce HttpClient.Send()? And in what context using it is good?

In MAUI, it is legitimate to load data inside the OnAppearing overrided:

    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class MainTabbedPage : TabbedPage
    {
        public MainTabbedPage ()
        {
            InitializeComponent();
        }

        protected override void OnAppearing()
        {
// calling HttpClient.Send()

I had thought the Send() function is a bit candy to replace SendAsync().Result in such context. Can you clarify please?

simonrozsival commented 2 months ago

@zijianhuang this is a good example of why we don't support the sync method on mobile. You shouldn't use the sync method or block until the async method completes in lifecycle method because it will freeze the UI until the request completes which might take a while.

Instead, you might want to start the request in OnAppearing but show some kind of a spinner or skeleton UI until the data is ready:

public static BindableProperty IsLoadingProperty = BindableProperty.Create(nameof(IsLoading), typeof(bool), typeof(MainTabbedPage), defaultValue: true);

public bool IsLoading
{
    get => (bool)GetValue(IsLoadingProperty);
    set => SetValue(IsLoadingProperty, value);
}

private Task? _request;

protected override void OnAppearing()
{
    _request = Task.Run(MakeRequest);
}

private async Task MakeRequest()
{
    IsLoading = true;

    // ...
    var response = await client.SendAsync(httpRequestMessage);
    // ...

    IsLoading = false;
}

Of course, you might also want to cancel the request in OnDisappearing and so on...

ManickaP commented 2 months ago

Send() function is a bit candy to replace SendAsync().Result

It's not. It's implemented (in SocketsHttpHandler) in a way that it calls sync methods all the way down, e.g. sync writes and reads on the socket. The motivation was Azure SDK, that already had a public sync API in which they were forced to do sync-over-async. So we introduced the sync Send for them and other similar use-cases, but with a bunch of caveats.

zijianhuang commented 2 months ago

@simonrozsival

Thanks for the handy example. I had probably used something similar long time ago, however, the following is a bit simpler, though probably a bit hacky:

public partial class MainTabbedPage : TabbedPage
{
    public MainTabbedPage ()
    {
        InitializeComponent();
    }

    protected override async void OnAppearing()
    {
        await LoadHeroes();
        base.OnAppearing();
    }

    async Task LoadHeroes()
    {
        var heroesVM = new VM.HeroesVM();
               await Task.Delay(5000);
        heroesVM.Load(await ClientApiSingleton.Instance.HeroesApi.GetHeroesAsync());
        await Task.Delay(5000);
        BindingContext = heroesVM;
    }
}

The UI is not blocked.

@ManickaP , thanks for the clarification, "The motivation was Azure SDK". So the solution could be simply adding a few more UnsupportedOSPlatform attributes so application programmers won't accidentally use it until hitting the wall when running on mobile devices..

ManickaP commented 2 months ago

So the solution could be simply adding a few more UnsupportedOSPlatform attributes so application programmers won't accidentally use it until hitting the wall when running on mobile devices..

Yep.

wfurt commented 2 months ago

The Send should sill work IMHO if you initiate HttpClient with SocketsHttpHandler @zijianhuang . The default use platform handler to be optimized for mobile platforms. However, as @ManickaP mentioned it should be used only when there is specific need and for example you would use support fort HTTP/2 and perhaps other functions.

BTW If you use the SendAsync in synchronous code use SendAsync().GetAwaiter().GetResult(). While sync over async is generally frown upon, I feel for small case like you probably have is just fine .e.g. unless you care much about scalability or performance.

zijianhuang commented 2 months ago

@wfurt

Yes, agree at some degree. There are wide variety of ways to use an async function in a sync context in .NET Framework, .NET Core and .NET. My context is that I had developed WebApiClientGen since 2015, generating C# client API providing both sync and async client APIs, though by default only async operations.

As you said, for something like HttpClient.Send().Result, there may be "small case like you probably have is just fine .e.g. unless you care much about scalability or performance", for example, an in-house fat .NET client talking to a LAN hosted Web service super fast and low latency. Also, handing for some PowerShell scripting with the generated client APIs.

And application programmers should continue to use HttpClient.SendAsync() as they have been doing since .NET era, well in line with JavaScript AJAX (fetch, Angular HttpClient etc.) supporting only async.

After all, the conversation above is great, now I seem to understand why the runtime team has provided only HttpClient.Send(), but no further expansion of such in the member methods of HttpClient.