Skip to content

Lifecycle of NSUrlSessionHandler CookieContainer vs the underlying NsUrlSession NSHttpCookieStorage.SharedStorage #23950

@valentinba

Description

@valentinba

Apple platform

iOS

Framework version

net9.0-*

Affected platform version

net9.0-ios

Description

In our MAUI iOS app we use HttpClient to talk to our API server. We configure the HttpClient with a custom HttpClientHandler. The custom handler only adds a few custom headers and delegates SendAsync to base.

When we create the client handler we set UseCookies to true and instantiate a new CookieContainer.

When the user signs in, the API server we talk to sends back an auth cookie (ASP.NET Identity). Any requests after logging in are authenticated via the auth cookie.

When the app restarts or wakes up after a given timeout expired, we start fresh by instantiating a new HTTP client along with its handler and cookie container, then prompt the user to log in again.

This may be a bit overkill but... this approach has worked fine when using Xamarin, and it does work when using MAUI on Android.

On MAUI iOS we've run into random issues when logging in as different users. Instead of using fresh new cookies obtained after signing in, the app appears to use old auth cookies, seemingly appearing out of nowhere.

After some debugging and digging in the source code it became apparent that the issue may be caused by the use of NSUrlSessionHandler as the default HTTP message handler on MAUI iOS.

NSUrlSessionHandler uses a default session configuration when creating the session. When UseCookies is set to true, the session cookie storage is set to the app domain shared cookie storage NSHttpCookieStorage.SharedStorage.

This native shared cookie storage appears to persists between app runs and it is likely the place from where the "stale" cookies come. We suspect that some of these cookies which are renewed if they hit the sliding expiration window cause the new cookies to be discarded somewhere in the native layer.

We validated this assumption by using <UseNativeHttpHandler>false</UseNativeHttpHandler> in the project file which switches to using the managed SocketsHttpHandler.

There appearsh to be a disconnect between the lifecycle of HttpClientHandler CookieContainer and NsUrlSession using default configuration with shared persistent cookie storage

The use of shared cookie storage is valid in scenarios that want to preserve authentication between app runs and across apps in the same domain. In our scenario this cookie caching between app runs or between different users logging in is unwanted and unexpected.

I think it would be beneficial to give users some control over the cookie storage strategy.

I don't know enough to make good recommendations but it seems that perhaps allowing for ephemeral sessions or at least some sort of an implementation of NSHttpCookieStorage other than SharedStorage could work.

There's a comment in the code that says that ephememeral sessions are not supported because they do not allow cookies. The documentation here https://developer.apple.com/documentation/foundation/urlsessionconfiguration/ephemeral says that ephemeral means "A session configuration that uses no persistent storage for caches, cookies, or credentials.". I read this to mean in memory storage, not no cookies allowed.

Found this old issue #5665 and this pull request #7654 that appear to have originally added support for CookieContainer in NSUrlSessionHandler.

Steps to Reproduce

I wrote a simple MAUI app to try isolate, debug and illustrate the issue. See the attached NoExtraCookiesForMePlease solution.
NoExtraCookiesForMePlease.zip

The app has just one screen with all the action in the Login button's click event (MainPage.xaml.cs OnLoginButtonClicked).

Clicking the Login button creates a new HttpClient, with a custom handler configured with a new CookieContainer.

Then the code hits two "APIs" running on an embedded web server running on localhost (inside the app) provided by EmbedIO. I chose this approach to simplify testing and debugging. It allowed me to see the cookies before and after the request, both in the client and the server.

The first API call does not expect nor return cookies. The second does set one cookie.

Build and run the app - I used an iOS simulator.

Set a few breakpoints in the custom http client handler SendAsync and in the two mock API implementations in TestController.

Click the Login button.

The first time you run the app, there will be no cookies in NSHttpCookieStorage.SharedStorage.
When the first API call is made, the managed CookieContainer will have no cookies.
Thus no cookies will be sent to the server.
After the second API call is made, the managed CookieContainer will have one cookie and NSHttpCookieStorage.SharedStorage will have one.

Stop the app and start debugging again.
Click the Login button.
When the first API call is made, the managed CookieContainer will have no cookies. However the response will have a cookie even thou the server did not send one. This is because the sneaky native session handler is looking in NSHttpCookieStorage.SharedStorage and sends that old cookie along.

Did you find any workaround?

As workaround we're considering clearing the shared NSHttpCookieStorage.SharedStorage at the time we're discarding the HttpClient and the message handler.
Alternatively, we may play with the managed client handler SocketsHttpHandler to assess any behavioral and performance differences.

This xamarin/Essentials#1712 along with the pull request xamarin/Essentials#1837 seemed somewhat conceptually related.

Relevant log output

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    need-attentionAn issue requires our attention/responsenetworkingIf an issue or pull request is related to networking

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions