Repository navigation
Cookies are not added to cookie container when running on iOS #2168
Description
Activity
Based on what I see at https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/HttpClientHandler.AnyMobile.InvokeNativeHandler.cs
This probably has something to do with the differences between the native handlers on iOS vs Android.
i.e. Xamarin.Android.Net.AndroidMessageHandler, Mono.Android vs System.Net.Http.NSUrlSessionHandler, Microsoft.iOSIf a redirect is involved, it might be related to #2059, although I wonder why the behavior would differ between mobile platforms. :(
Thank you for your response and looking into the post.
In fact I do not suspect the issue comes from those handlers because eventually due to the cookie issue I had withRestsharp, I replaced it with the nativeHttpClientand it works fine on both platforms.
I put an example of the working code below.private static readonly System.Net.Http.HttpClientHandler HttpClientMessageHandler = new() { AllowAutoRedirect = false, UseCookies = true, CookieContainer = new(), }; private static readonly System.Net.Http.HttpClient RestClient = new(HttpClientMessageHandler) { BaseAddress = new("https://google.com"), Timeout = TimeSpan.FromSeconds(60), DefaultRequestHeaders = { { "User-Agent", "mobile"}, { "Accept", "application/json" } } };The one can simply use
var response = await RestClient.GetAsync();@h-arshad RestSharp handles cookies without using an external
CookieContainer. It might have worked if you have just removed the cookie container from the client setup.It could also be that .NET on iOS has some differences concerning getting cookies from the container based on the URL.
Basically, what RestSharp does: it creates a new cookie container for each request, unless the request itself has a cookie container. But, the container is not used for making a call. Instead, RestSharp adds cookie headers to the request.
When RestSharp receives a response withSetCookieheaders, it adds those values to the cookie container.The code looks like this:
public static void AddCookies(this CookieContainer cookieContainer, Uri uri, IEnumerable<string> cookiesHeader) { foreach (var header in cookiesHeader) { try { cookieContainer.SetCookies(uri, header); } catch (CookieException) { // Do not fail request if we cannot parse a cookie } } }
You see there that sometimes adding a cookie to the container fails. I am not sure if that's what happens on iOS.
@h-arshad I have the same issue with RestClient not returning the cookies on MAUI for iOS.
I was able to resolve it by passing an instance of HttpClient which had the
CookieContainerset on its HttpClientHandler (similar to the working code you posted above).I think setting it on the handler via RestClientOptions.ConfigureMessageHandler should probably work too, though i haven't tested it.
The below code would probably also work with IHttpClientFactory, by using that to create the HttpClient instance(and configuring the CookieContainer), but again not tested.
var handler = new HttpClientHandler { CookieContainer = _cookieManager }; var client = new HttpClient(handler); var options = new RestClientOptions() { BaseUrl = new Uri(Connection.Url), Timeout = TimeSpan.FromSeconds(60), CookieContainer = _cookieManager, }; return new RestClient(client, options);@alexeyzimarev its feels like the
ConfigureHttpMessageHandlermethod should be applyingOptions.CookieContainerto the handler, even though you say that it doesn't.Setting the CookieContainer on the request didn't fix the problem on iOS MAUI, neither did not setting
Options.CookieContainerstatic void ConfigureHttpMessageHandler(HttpClientHandler handler, RestClientOptions options) { #if NET if (!OperatingSystem.IsBrowser()) { #endif handler.UseCookies = false; handler.Credentials = options.Credentials; handler.UseDefaultCredentials = options.UseDefaultCredentials; handler.AutomaticDecompression = options.AutomaticDecompression; handler.PreAuthenticate = options.PreAuthenticate; if (options.MaxRedirects.HasValue) handler.MaxAutomaticRedirections = options.MaxRedirects.Value; if (options.RemoteCertificateValidationCallback != null) handler.ServerCertificateCustomValidationCallback = (request, cert, chain, errors) => options.RemoteCertificateValidationCallback(request, cert, chain, errors); if (options.ClientCertificates != null) { handler.ClientCertificates.AddRange(options.ClientCertificates); handler.ClientCertificateOptions = ClientCertificateOption.Manual; }It's weird that the issue only affects one particular platform. RestSharp doesn't do any kind of magic with cookies, and the reason why it doesn't set the cookie container property of the handler is because otherwise handling cookies will deviate from what's already there when the container isn't used.
Basically, after making a call, RestSharp tries to find cookie headers, collect the values, and add cookies to the response and to the
Options.CookieContainer. It's really strange that it doesn't work on iOS.// Parse all the cookies from the response and update the cookie jar with cookies if (responseMessage.Headers.TryGetValues(KnownHeaders.SetCookie, out var cookiesHeader)) { // ReSharper disable once PossibleMultipleEnumeration cookieContainer.AddCookies(url, cookiesHeader); // ReSharper disable once PossibleMultipleEnumeration Options.CookieContainer?.AddCookies(url, cookiesHeader); }
@alexeyzimarev Thank you for the suggestion. I tried to make the request on sample code without initializing the cookie container, but it did not work. In fact, after looking into the response object and comparing the Android and iOS, I realized that on iOS the
ResstSharp.RestResponseobject has 9 headers and Android has 17 headers after the call completes. Strangely enough, on iOS there are no headers forSet-Cookie, it seems the cookie container is not the main problem after all.Reviving this with a definitive root-cause analysis after #2385 came in with the cleanest reproducer yet (side-by-side
HttpClientvsRestClientdiagnostic dump, exact same endpoint, only the iOS RestSharp response is missingSet-Cookie).What's actually happening
On iOS / Mac Catalyst,
HttpClientis backed byNSUrlSessionHandler, which wraps Apple'sNSURLSession. By default:NSURLSession.HTTPCookieStorage=NSHTTPCookieStorage.SharedStorage(process-wide singleton)NSURLSession.HTTPCookieAcceptPolicy=Always
When a response with
Set-Cookiearrives, NSURLSession consumes the cookie intoNSHTTPCookieStorageand strips the header before passing the response up to .NET. By the time RestSharp'sParseResponseCookiesdoesresponseMessage.Headers.TryGetValues("Set-Cookie", ...), the header is gone.HttpClientHandler.UseCookies = false(RestSharp's default) does not prevent this on iOS — that flag only controls the managedHttpClientHandler's cookie integration, not NSURLSession's native cookie handling underneath.Android doesn't have this problem because
AndroidMessageHandlerdoesn't interceptSet-Cookiethe same way. Same code, different native behaviour.The reason @h-arshad's direct-
HttpClientworkaround appears to work is misleading: settingUseCookies = true+ a managedCookieContaineronHttpClientHandlerre-routes cookies through a path that exposes them via the container. The original header is still stripped — the cookies are just visible somewhere else.Why the "obvious" fix is wrong for RestSharp
The straightforward fix would be: have RestSharp set
handler.UseCookies = true+ give it a sharedCookieContainer, then read cookies from the container after each call. We can't do this. RestSharp deliberately setsUseCookies = falseand avoids handing the handler a shared container, because in multi-tenant scenarios (oneRestClientinstance serving requests for different users/tenants) a shared container pools cookies across every call. Tenant A's authentication cookie ends up on tenant B's request. This was a deliberate design decision and is not negotiable.The actual fix
Suppress NSURLSession's native cookie handling entirely, so the
Set-Cookieheader passes through to .NET intact. RestSharp's existing per-requestParseResponseCookiesthen runs as designed, with cookies stored only in the per-requestCookieContainer. No shared state anywhere.The recipe (iOS / MAUI consumer applies it via
ConfigureMessageHandler):#if IOS || MACCATALYST using Foundation; var options = new RestClientOptions(baseUrl) { ConfigureMessageHandler = _ => { var config = NSUrlSessionConfiguration.DefaultSessionConfiguration; config.HttpCookieStorage = null; config.HttpCookieAcceptPolicy = NSHttpCookieAcceptPolicy.Never; return new NSUrlSessionHandler(config); } }; #endif
What this does:
HttpCookieStorage = null→ NSURLSession has nowhere to put cookies.HttpCookieAcceptPolicy = Never→ NSURLSession doesn't even try to interpretSet-Cookie.- Header reaches
responseMessage.Headersintact, RestSharp's parser handles it normally. - Multi-tenant safety preserved: no shared cookie state at any layer.
Why this can't go into core RestSharp
The recipe needs to call
NSUrlSessionConfigurationandNSUrlSessionHandler, which only exist in the iOS workload (net9.0-iosetc.). CoreRestSharptargetsnetstandard2.0and the standard .NET TFMs — pulling iOS workloads in would penalise every non-iOS consumer.Follow-ups
- Docs: iOS/MAUI cookie handling workaround #2387 tracks adding the recipe to the docs site (cookies / advanced topics page).
- RFC: RestSharp.Maui companion package for platform-specific quirks #2388 is an RFC for a small
RestSharp.Mauicompanion package that would expose this (and future platform-specific quirks) as one-liner extension methods likeoptions.UseIosCookieFix().
Keeping this issue open as the canonical thread; the actionable work has moved to the two follow-ups. Closing #2385 as duplicate.
Reacted by Filip Brzek
When using rest sharp in NET MAUI application development, for the same API call that is supposed to inject a cookie, in Android you will see the cookie but it is missing when running in iOS device.
Code Sample:
Expected behaviour:
The response cookie should be added inside the cookie container.