iOS 11.3: WKWebView cookie synchronizing stopped working

Hi,


Anyone else having issues with WKWebView cookies after iOS 11.3? (Or any recomondations of what we might be doing wrong?)


Working on an app with web content, we switched fro UIWebView to WKWebView after iOS 11.

We use URLSession for authentication, then webviews to show content.


In order to syncrhonzie cookies to WKWebViews, we have used the following procedure:


1. Log in with URLSession

2. Create a WKWebView with the cookies from the URLSession:

let config = WKWebViewConfiguration()
config.processPool = WKProcessPool()
let cookies = HTTPCookieStorage.shared.cookies ?? [HTTPCookie]()
cookies.forEach({ config.websiteDataStore.httpCookieStore.setCookie($0, completionHandler: nil) })
          
let wkWebView = WKWebView(frame: bounds, configuration: config)
...


3. Load the URL.


Worked like a charm, until iOS 11.3.


Something fishy is happening, I have checked that the HTTPCookieStorage contains the correct cookies, and verified that the completionHandler of "setCookie" gets called before the url loads. However, the cookies are not being set.


But. If we wait with creating the WKWebView/transfering cookies by about ~3 seconds after loggign in, it seems to work.


This might seem like a bug introduced in iOS 11.3 (?), or any other suggestions?

boedev, I have exactly the same issue. Were you able to find any solution for that?

Did you tried to load WKWebView after cookies setting completion?
Try to create dispatch group and create/load WkWebView in group.notify handler

it doesn't work on iOS 11.3. I'm creating web view after cookies injection and with sharing the same Pool. On iOS 11.2.6 it works as expected...

Someone recently opened a DTS incident with a report of a similar problem, which gave me the opportunity to try it out. In my tests I saw the cookie show up on 11.3 just fine. So there’s something specific about your app that’s triggering this.

Did you try what dskibin suggested, namely, using a dispatch group to guarantee that your completion handlers are all called before you load up the web view?

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thanks for jumping on this issue.

We are using RxSwift for async flow management instead of dispatch group. And I'm pretty sure that all injections happened before web view loaded.


A few interesting notes about this:

1) injection works well on both 11.2.6. and 11.3. I could see cookies in the httpCookieStore

2) on 11.2.6 cookies are attached to the web page as expected

3) on 11.3 WkWebView just ignore them

4) adding the delay in web view load for 3-10 seconds doesn't fix the issue

5) after the application start web view loaded without cookies but if I change user (try to set new cookies) then cookies become attachable.

eskimo, here is the code snippet with dispatch group that works nice on iOS 11.2.6 and stop working on 11.3


    override func viewDidLoad() {
        super.viewDidLoad()
     
        let pool = WKProcessPool()
        let configuration = WKWebViewConfiguration()
        configuration.processPool = pool
     
        let webView = WKWebView(frame: view.bounds, configuration: configuration)
        view.addSubview(webView)
     
        let sem = DispatchGroup()
        cookies.forEach { cookie in
            sem.enter()
            configuration.websiteDataStore.httpCookieStore.setCookie(cookie, completionHandler: {
                sem.leave()
            })
        }
     
        sem.notify(queue: .main) {
            let url = URL(string: "https://some-private-link.com")
            let request = URLRequest(url: url)
            webView.load(request)
        }
    }

    fileprivate var cookies: [HTTPCookie] {
       
        let isEnvSecured = true
       
        var emailCookieProperty: [HTTPCookiePropertyKey: Any] = [
            HTTPCookiePropertyKey.domain: "some-private-link.com",
            HTTPCookiePropertyKey.path: "/",
            HTTPCookiePropertyKey.name: "X-User-Email",
            HTTPCookiePropertyKey.value: "some@example.com"]
       
        if isEnvSecured {
            emailCookieProperty[HTTPCookiePropertyKey.secure] = "Yes" as NSString
        }
       
        var tokenCookieProperty: [HTTPCookiePropertyKey: Any] = [
            HTTPCookiePropertyKey.domain: "some-private-link.com",
            HTTPCookiePropertyKey.path: "/",
            HTTPCookiePropertyKey.name: "X-User-Token",
            HTTPCookiePropertyKey.value: "N9m6M7mT-tm9yFVDVoLP"]
       
        if isEnvSecured {
            tokenCookieProperty[HTTPCookiePropertyKey.secure] = "Yes" as NSString
        }
       
        let emailCookie: HTTPCookie! = HTTPCookie(properties: emailCookieProperty)
        let tokenCookie: HTTPCookie! = HTTPCookie(properties: tokenCookieProperty)
        return [emailCookie, tokenCookie]
    }


Do you see any potential issues with it? Could you please share your own code snippet so we could check that locally as well?

I am facing similar issue. I need to set an authentication cookie and the code works fine on iOS 11.2 I am able to see in Charles that the cookie is set and I am able to open the authenticated webpage in WKWebView but the same code doesnt work on iOS11.3 Even in charles I see that the cookie is not set. Below is what I am trying:


//Create webview
let webView = WKWebView(frame: .zero, configuration: WKWebViewConfiguration())
//Add webview to the UI
//....
//Set the cookie
let authCookie = HTTPCookie.init(properties: [HTTPCookiePropertyKey.name: "OAuth.AccessToken", HTTPCookiePropertyKey.value: "some_authToken", HTTPCookiePropertyKey.domain: ".somedomain.com", HTTPCookiePropertyKey.path: "/", HTTPCookiePropertyKey.secure: "TRUE", HTTPCookiePropertyKey.expires: Date.distantFuture])
if let reqCookie = authCookie {
     webView.configuration.websiteDataStore.httpCookieStore.setCookie(reqCookie) {
          DispatchQueue.main.async {
               self.webView.load(checkoutRequest)
          }
     }
}

As I mentioned back on 12 Apr, I tried this here in my office and it worked for me. I’m not sure why it’s failing in some cases and not others. If you’d like me to investigate your specific case, you should open a DTS tech support incident and we can pick things up in that context.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

I am facing the same issue on iOS 11.3. Same code works fine on 11.2.

The problem seems to be something with threads, if I add an observer to the cookie store with the following code:


// In viewDidLoad
let authCookie = HTTPCookie.init(properties: [HTTPCookiePropertyKey.name: "OAuth.AccessToken", HTTPCookiePropertyKey.value: "some_authToken", HTTPCookiePropertyKey.domain: ".somedomain.com", HTTPCookiePropertyKey.path: "/", HTTPCookiePropertyKey.secure: "TRUE", HTTPCookiePropertyKey.expires: Date.distantFuture])|
WKWebsiteDataStore.default().httpCookieStore.add(self)
WKWebsiteDataStore.default().httpCookieStore.setCookie(authCookie)

public func cookiesDidChange(in cookieStore: WKHTTPCookieStore) {
     print("Cookies did change")
     cookieStore.getAllCookies { (cookies) in
          print("Cookie exists?: \(cookies.contains(where: { $0.name == "OAuth.AccessToken" }))")
     }
     DispatchQueue.main.async {
          cookieStore.getAllCookies { (cookies) in
               print("Cookie exists on main thread?: \(cookies.contains(where: { $0.name == "OAuth.AccessToken" }))")
          }
     }
}


Then the output is:


Cookies did change
Cookie exists?: true
Cookie exists on main thread?: false


As the webview always loads requests on the main thread, it doesn't see the cookie in the storage so it never gets sent.

I also have the same problem.

@Soumin and @nRewik, my offer from 30 Apr is still open.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Hey @eskimo and others: We had the same problem here, and I believe I've found the true cause and solution. This change (https://github.com/WebKit/webkit/commit/d9d6e5c82c4a74cd573f3f119f166ffcee477b04) caused WebKit to prevent initializing the WKWebsiteDataStore until necessary. We have a flow in our app where, if the webview attempts to navigate to a particular URL that requires a cookie, we cancel the navigation, download and set a cookie, and in the cookie setter's completion handler, load the original request (this time with the cookie).


However, since we've canceled the navigation, the WKWebView doesn't see the creation of the data store "necessary" and queues our completion handler. The URL is never loaded, since our completion handler is never called. It's almost like a deadlock.


Since this isn't the behavior I'd expect, I filed this as rdar://40100673 and https://bugs.webkit.org/show_bug.cgi?id=185483.


Here's our problematic code, and a workaround, which forces the creation of the WKWebsiteDataStore:

    if (@available(iOS 11.0, *)) {
        [webView.configuration.websiteDataStore.httpCookieStore setCookie:cookie
                                                        completionHandler:^{
            [webView loadRequest:request];
        }];

        // WORKAROUND: Force the creation of the datastore by calling a method on it.
        [webView.configuration.websiteDataStore fetchDataRecordsOfTypes:[NSSet<NSString *> setWithObject:WKWebsiteDataTypeCookies]
                                                      completionHandler:^(NSArray<WKWebsiteDataRecord *> *records) {}];
    }

Thanks for the heads up. I now have a couple of folks who’ve opened incidents about this. We haven’t yet got to the stage where I can tell why their apps are failing while my test app works, but I’ll keep your scenario in mind.

Since this isn't the behavior I'd expect, I filed this as rdar://40100673 …

And thanks for that too.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

I tired to remove the cookies before loading the webview like this. It works for me. But I do not want to remove the cookies everytime I load the webview. So its workaround not a solution.


            let group = DispatchGroup()
            group.enter()
            webView.configuration.websiteDataStore.removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), modifiedSince: .distantPast) {
               group.leave()
           }

            group.notify(queue: .main) {
                self.fillWebViewCookieStore(webView.configuration.websiteDataStore.httpCookieStore) {
                    webView.load(request)
                }
            }

Thanks for your input!


We also solved our problem by calling the websiteDataStore before creating the webview, seems like the commit you are talking about might be the problem.


We have kind of the same flow, as mentioned in the main post:


1. (native) login with URLSession

1,5: "Hack": call websiteDataStore

2. transfer cookies from HTTPStorage to WKWebView

3. navigate to URL in WKWebview that requires a cookie set


If we don't do step 1,5, the cookies for the WKWebView in the "decidePolicyFor" delegation method is wrong


if #available(iOS 11.3, *) {
    let _ = WKWebViewConfiguration().websiteDataStore
}


Saw the ticked you made @hshamanskygot an update a few days ago. We have teste with the latest version of iOS 12 (beta 4) - seems like the issue is still there.


Any updates on your finds @eskimo?


The sad part for us, is that it seems like this will fix the issue 99% of the times. It's hard to tell when it fails, and we can't reproduce it, it just fails now and then.

Any updates on your finds @eskimo?

I had a look at the various bugs clustered around this issue and none of them indicate a clear fix. If you have a workaround that works for you, that’s cool. In my testing I was able to find various workarounds that work in various circumstances, but nothing universal )-:

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Okey – I guess we'll live with the workaround for now, and hope for a fix in the future. Thanks for your update 🙂

I can confirm this problem still exists in the latest iOS12 Release version.


Our worklfow is quite simple:

We basically have a viewcontroller with a webview.

From a second viewcontroller we navigate to the webview-viewcontroller.

There we call "webview.configuration.websiteDataStore.httpCookieStore.setCookie(XXX)"

After its completion, we load the webview.



So I went trough a lot of debugging and setting breakpoints and what i found out: on the first appstart (quit app in backround and make a fresh start), when the viewcontroller with the webview is loaded the first time, the following happens:


- webview.configuration.websiteDataStore.httpCookieStore is empty, which is correct

- we call webview.configuration.websiteDataStore.httpCookieStore.setCookie(XXX) to set our cookie

- the completion handler gets called.

- after short delay the "func cookiesDidChange(in cookieStore: WKHTTPCookieStore)" gets called, which is also expected


- so with the completionhandler of "setCookie" complete and even the observer "cookiesDidChange" triggering, we can expect that the cookie was indeed set. So now we check the content of the "webview.configuration.websiteDataStore.httpCookieStore" and what does it show? -> It's empty. No cookies where set even tough the completion handler was called and "cookiesDidChange" reported a change.


This only happens on fresh app starts and the first time the above process is triggered. Even when the webview and its viewcontrollers get dismissed later and reinstanciated with the same process, setting the cookies will work as expected. So i guess there is indeed some problem with cookie storage not beeing initiated correctly from the system on a fresh app start until the first time its actually used in some way. And while the "setCookie" completionhandler fires and the "cookiesDidChange" gets triggered, the cookies is never really set.


I can also confirm that the mentioned workaround seems to help. So what we do now is: When the viewcontroller with the webview gets initiated, in viewWillAppear we call "webview.configuration.websiteDataStore.httpCookieStore.getAllCookies()". We dont store the returned values we just call the method. Just making this call seems to prevent the above problem to appear and the cookies will get set as expected.

I can confirm this problem still exists in the latest iOS12 Release version.

It sounds like you’ve done a really good job of isolating this problem. If you can boil that down into a small test project and attach that to a bug report, I’d be most grateful.

Please post your bug number, just for the record.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Hi, i will do so as soon as possible.


But I can give an update to my workflow:

It seems like my solution by calling "webview.configuration.websiteDataStore.httpCookieStore.getAllCookies()" did not work as reliable as it should have. So I overhauled my code and at least fo now, it seems like i finally found a solution for my situation:


It seems like it makes a difference if you try to add cookies to an existing WKWebview or if you create one after setting the cookies. Somewhere on stackoverflow people reported that an already existing WKWebview sometimes ignores set cookies (at least on a fresh appstart)


So we are now using the following workflow (code simplified):

//Instead of adding the cookies to an already existing instance of a WKWebview
//We create the webview only !after! setting all cookies:

//Create a new WKWebViewConfiguration and a new WKWebsiteDataStore
let config = WKWebViewConfiguration()
let wkDataStore = WKWebsiteDataStore.nonPersistent()

//Get sharedCookies from HTTPCookieStorage
let sharedCookies: Array<HTTPCookie> = HTTPCookieStorage.shared.cookies(for: webviewURL) ?? []
let dispatchGroup = DispatchGroup()

//Add each cookie to the created WKWebsiteDataStore
//Wait until all setCookie completion handlers are fired and proceed
if sharedCookies.count > 0 {
    //Iterate over cookies in sharedCookies and add them to webviews httpCookieStore
    for cookie in sharedCookies{
        dispatchGroup.enter()
        wkDataStore.httpCookieStore.setCookie(cookie){
            dispatchGroup.leave()
        }
    }
    
    //Wait for dispatchGroup to resolve to make shure that all cookies are set
    dispatchGroup.notify(queue: DispatchQueue.main) {
      //Add datastorage to configuration
      config.websiteDataStore = wkDataStore
      //Now crate a new instance of the wkwebview and add our configuration with the cookies to it
      self.webview = WKWebView(frame: self.view.bounds, configuration: config)
      self.webview!.navigationDelegate = self
            
      self.view.addSubview(self.webview!)
      self.webview!.load(request)
    }
}
else{
     //do something else
}


For now it seems like the above workflow finally brought the solution.

This is a bit of a retrospective, but we have just been bitten terribly by the bug/change. We read a million articles / this forum / stackoverflow questions that all seemed to stem from cookie behavior changing in 11.3/11.4/12+, I've read this forum dozens of times. Our app got hammered by negative reviews, as users continuously kept getting logged out as the cookies stopped working consistently, and users were tweeting and calling our support center. Our QA process could not reproduce what was going on, we haven't pushed an App Store update to our app in months. Sometimes it worked, sometimes force closing worked, sometimes when you logout/login-as-another-user, then force close, and re-enter the app, you were logged in as the previous user, chaos...


Tangent: I'd like to complain that in iOS that there isn't just one best "WebView" class. It got splintered into UIWebView, and WkWebView, and app developers had to deal with intricacies related to managing networking, and cookies with each implementation. (Android did have a parallel chrome webview, but it got merged into just WebView, and networking and cookies are all managed by the OS, nobody had to carry an albatross of code managing which-webview-on-which-version-and-how-to-migrate). Since we want to support users with as many versions of iOS, we chose to build a WebViewAbstraction that allowed us to use the best WebView implementation per OS version. We then also synced cookies back to UserDefaults so that in case the user upgrades iOS versions, we would have a way of reading and migrating cookies. We then had to overload cookiesDidChange, we had to manage our cookie stores. And this rube-goldberg machine worked until iOS 11.3/11.4/12.0. Without any big announcement, that I'm aware of, very subtley, users who upgrade iOS, get random logout/cookie issues. Impossible to consistently reproduce.


Here's some psuedo-code of what our app used to be:

#onStart
if(iOS >= 11) {
  webviewWrapper = WKWebView()
  readCookiesFromUserDefaultsIntoWKWebView()
} else {
  webviewWrapper = UIWebView()
  readCookiesfromUserDefaultsIntoUIWebView()
}

// Our cookies expire after an hour, and the app-server sends updated cookies, by consuming a token
cookiesDidChange() {
  syncCookiesFromWebViewToUserDefaults()
}

#onStop
saveCookiesFromWebviewToUserDefaults()


We also have HTTPS requests that our app has to make, and we have to wire it up with the correct session/cookieStore for which webview we're using. And when those HTTP requests complete, they then have to write new cookies into the cookieStore.


Our Solution:

- Drop support for iOS < 11. The WkWebView has presentation bugs in iOS < 11. The UIWebView has presentation bugs galore, and doesn't work well with our HTML app. And this whole debacle made clear that managing our own cookies is frought with adversity. We then removed the syncing to/fro UserDefaults. We removed UIWebView, we removed the WebViewWrapper, we removed our CookieStoreAbstractor. Very vanilla WkWebView inside our ViewController. We don't loadCookies, we don't setCookies, we don't cookiesDidChange, none of it.


When our HTTP requests have to read current cookies I manually construct a http header "cookieOne=1; cookieTwo=2". When the HTTP request responses have something to write to cookies, thats the only piece of magic we have:


let allHeaderFields = httpResponse.allHeaderFields as! [String : String]
let cookies = HTTPCookie.cookies(withResponseHeaderFields: allHeaderFields, for: URL(string: self.configuration.environment.cookieDomain)!)

// Need to run writing cookies on the "main thread". Load homepage when all writes are done (enter/leave/notify).
DispatchQueue.main.async {
  let dispatchGroup = DispatchGroup()

  for cookie in cookies {
    dispatchGroup.enter()
    webView.wkWebView!.configuration.websiteDataStore.httpCookieStore.setCookie(cookie, completionHandler: {
      dispatchGroup.leave()
    })
  }

  dispatchGroup.notify(queue: .main) {
    // load the home page into the web view.
    webView.loadURL(urlRequest: URLRequest(url: URL(string: self.configuration.environment.mainURL)!))
  }
}


TL;DR; Custom cobbling a cookie solution is broken on iOS 11.3/11.4+, so we fixed it by removing everything, and just using WkWebView default, and our cookies aren't broken any more...


Its been a terrible couple of weeks of working all day and all night trying to find out what went wrong, where are the race conditions, and exploring every possible solution, until we just looked at what would be the simplest solution, and it involved nuking everything. I hope this post helps the next person.

The problem with syncing cookies to WKWebView lies in WKProcessPool. To properly sync cookie, you have to create an instance of WKProcessPool and set it to the WKWebViewConfiguration that is to be used to initialize the WkWebview itself:


    private lazy var mainWebView: WKWebView = {
        let webConfiguration = WKWebViewConfiguration()
        if Enviroment.shared.processPool == nil {
            Enviroment.shared.processPool = WKProcessPool()
        }
        webConfiguration.processPool = Enviroment.shared.processPool!
        webConfiguration.processPool = WKProcessPool()
        let webView = WKWebView(frame: .zero, configuration: webConfiguration)
        webView.navigationDelegate = self
        return webView
    }()


Setting WKProcessPool is the most important step here. WKWebview makes use of process isolation - which means it runs on a different process than the process of your app. This can sometimes cause conflict and prevent your cookie from being synced properly with the WKWebview. If you don't use the same instance of WKProcessPool each time you configure a WKWebView for the same domain (maybe you have a VC A that contains a WKWebView and you want to create different instances of VC A in different places), there can be conflict setting cookies. To solve the problem, after the first creation of the WKProcessPool for a WKWebView that loads domain B, I save it in a singleton and use that same WKProcessPool every time I have to create a WKWebView that loads the same domain B


After the initialization process, you can load an URLRequest inside the completion block of httpCookieStore.setCookie. Here, you have to attach the cookie to the request header otherwise it won't work.


    mainWebView.configuration.websiteDataStore.httpCookieStore.setCookie(your_cookie) {
            self.mainWebView.load(your_request, with: [your_cookie])
    }


    extension WKWebView {
       func load(_ request: URLRequest, with cookies: [HTTPCookie]) {
          var request = request
          let headers = HTTPCookie.requestHeaderFields(with: cookies)
          for (name, value) in headers {
             request.addValue(value, forHTTPHeaderField: name)
          }      
          load(request)
       }
    }

eskimo,


If you have a working sample, can you please share your code sample? Thanks!

Hello!


I finally solve this problem successfully,



I used the webView navigationResponse: WKNavigationResponse to get cookies and save them to local storage, and every time the application started, I consulted the localstorage to get saved cookies and inject them into httpCookieStore before the first load of the webView



I hope this can help sameone.



Att,

Khalid Ait Bellahs

https://www.tialtonivel.com.br

iOS 11.3: WKWebView cookie synchronizing stopped working
 
 
Q