xamarin / xamarin-macios

.NET for iOS, Mac Catalyst, macOS, and tvOS provide open-source bindings of the Apple SDKs for use with .NET managed languages such as C#
Other
2.44k stars 507 forks source link

Unable to clear HttpClient cookies #20901

Closed kerams closed 1 month ago

kerams commented 1 month ago

Steps to Reproduce

  1. Create a static HttpClient with a handler that has UseCookies set.
  2. Send some request.
  3. Try to clear the cookies by doing httpClient.Handler.CookieContainer = new() since there is no instance method like Clear.

Expected Behavior

Cookies are cleared, just like on other platforms (Android, Windows). I suppose I could create a new HttpClient instead, but I don't see why this should not be permitted.

Actual Behavior

System.InvalidOperationException: This instance has already started one or more requests. Properties can only be modified before sending the first request.

Environment

Version information - SDK `9.0.0-preview.6.24327.7` - TFM `net9.0-ios` - RID `iossimulator-arm64` - iPhone 15, iOS 17.5 simulator
kerams commented 1 month ago

Hm, I seemingly can't read cookies either - Handler.CookieContainer.GetCookies. To what degree are cookies implemented on iOS?

rolfbjarne commented 1 month ago

The InvalidOperationException seems expected to me, I get the same behavior with a .NET 8 console project:

using System.Net;

class App
{
    async static Task<int> Main ()
    {
        await TestCookieContainerAsync ();
        return 0;
    }

    async static Task TestCookieContainerAsync ()
    {
        var handler = new SocketsHttpHandler ();
        handler.UseCookies = true;
        var uri = new Uri ("https://httpbin.org");

        var container = new CookieContainer ();
        container.Add (uri, new Cookie ("a", "b"));
        handler.CookieContainer = container;

        var httpClient = new HttpClient (handler);
        var result = await httpClient.GetAsync ("https://httpbin.org/cookies/set/c/d");

        Console.WriteLine (result);
        var cookies = container.GetCookies (uri);
        Console.WriteLine ($"{cookies.Count ()} cookies: {string.Join (";", container.GetCookies (uri).Select (v => $"{v.Name}={v.Value}"))}");

        try {
            container = new CookieContainer ();
            container.Add (uri, new Cookie ("e", "f"));
            handler.CookieContainer = container;

            result = await httpClient.GetAsync ("https://httpbin.org/cookies/set/g/h");
            Console.WriteLine (result);
            cookies = container.GetCookies (uri);
            Console.WriteLine ($"{cookies.Count ()} cookies: {string.Join (";", container.GetCookies (uri).Select (v => $"{v.Name}={v.Value}"))}");
        } catch (Exception e) {
            Console.WriteLine (e);
        }
    }
}
$ dotnet run
StatusCode: 200, ReasonPhrase: 'OK', Version: 1.1, Content: System.Net.Http.HttpConnectionResponseContent, Headers:
{
  Date: Mon, 05 Aug 2024 17:38:09 GMT
  Connection: keep-alive
  Server: gunicorn/19.9.0
  Access-Control-Allow-Origin: *
  Access-Control-Allow-Credentials: true
  Content-Type: application/json
  Content-Length: 51
}
2 cookies: a=b;c=d
System.InvalidOperationException: This instance has already started one or more requests. Properties can only be modified before sending the first request.
   at System.Net.Http.SocketsHttpHandler.CheckDisposedOrStarted()
   at System.Net.Http.SocketsHttpHandler.set_CookieContainer(CookieContainer value)
   at App.TestCookieContainerAsync() in /Users/rolf/test/dotnet/console/Program.cs:line 31

It seems that setting Expired=true for each cookie is the way to "delete" them:

using System.Net;

class App
{
    async static Task<int> Main ()
    {
        await TestCookieContainerAsync ();
        return 0;
    }

    async static Task TestCookieContainerAsync ()
    {
        var handler = new SocketsHttpHandler ();
        handler.UseCookies = true;
        var uri = new Uri ("https://httpbin.org");

        var container = new CookieContainer ();
        container.Add (uri, new Cookie ("a", "b"));
        handler.CookieContainer = container;

        var httpClient = new HttpClient (handler);
        var result = await httpClient.GetAsync ("https://httpbin.org/cookies/set/c/d");

        Console.WriteLine (result);
        var cookies = container.GetCookies (uri);
        Console.WriteLine ($"{cookies.Count ()} cookies: {string.Join (";", container.GetCookies (uri).Select (v => $"{v.Name}={v.Value}"))}");

        try {
            foreach (Cookie cookie in container.GetCookies (uri))
                cookie.Expired = true;

            result = await httpClient.GetAsync ("https://httpbin.org/cookies/set/g/h");
            Console.WriteLine (result);
            cookies = container.GetCookies (uri);
            Console.WriteLine ($"{cookies.Count ()} cookies: {string.Join (";", container.GetCookies (uri).Select (v => $"{v.Name}={v.Value}"))}");
        } catch (Exception e) {
            Console.WriteLine (e);
        }
    }
}
$ dotnet run
StatusCode: 200, ReasonPhrase: 'OK', Version: 1.1, Content: System.Net.Http.HttpConnectionResponseContent, Headers:
{
  Date: Mon, 05 Aug 2024 17:39:32 GMT
  Connection: keep-alive
  Server: gunicorn/19.9.0
  Access-Control-Allow-Origin: *
  Access-Control-Allow-Credentials: true
  Content-Type: application/json
  Content-Length: 51
}
2 cookies: a=b;c=d
StatusCode: 200, ReasonPhrase: 'OK', Version: 1.1, Content: System.Net.Http.HttpConnectionResponseContent, Headers:
{
  Date: Mon, 05 Aug 2024 17:39:33 GMT
  Connection: keep-alive
  Server: gunicorn/19.9.0
  Access-Control-Allow-Origin: *
  Access-Control-Allow-Credentials: true
  Content-Type: application/json
  Content-Length: 36
}
1 cookies: g=h

So I'm closing this, as it seems our current behavior is correct. If you disagree, feel free to reopen and provide a complete test project we can use to reproduce the behavior.