NSURLSession's NSURLCache not working as expected - ios

I'm seeing weird behaviors when using NSURLCache and exhausted all options but posting here... so here it is:
I'm trying to implement NSURLCache as underlying caching mechanism in combination with NSURLSession. And using Charles and simple println() I observe that no matter what I do, my app continues to go back to the server and re-requests previous requests.
I validated that my server sends the following headers consistently:
Cache-Control:public, max-age=31449600
Content-Length:74289
Content-Type:image/jpeg
ETag:"abcdf"
Expires:Sun, 03 Jan 2016 20:58:01 GMT
Last-Modified:Sat, 09 Nov 2013 08:05:53 GMT
Yet when I make the following request:
let req = NSURLRequest(URL: NSURL(string: URLString)!, cachePolicy: .ReturnCacheDataElseLoad, timeoutInterval: 15)
NSURLSession's willCacheResponse: delegate method doesn't get called at all:
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, willCacheResponse proposedResponse: NSCachedURLResponse, completionHandler: (NSCachedURLResponse!) -> Void) {
However, when simply requesting NSURLRequest(URL: NSURL(string: URLString)!) without specifying specific cachePolicy, then willCacheResponse: does get called. Nevertheless, the next time the same request is made, the app goes back to server...
I don't do any response object modifications in any NSURLSession delegate methods and my requests are all HTTP GET.
What am I missing?
I've read a lot that's out there on NSURLCache:
https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/URLLoadingSystem/Concepts/CachePolicies.html
http://nshipster.com/nsurlcache/
http://petersteinberger.com/blog/2012/nsurlcache-uses-a-disk-cache-as-of-ios5/
NSURLCache doesn't cache
various other SO threads...
Setup: Xcode6.1, Swift, iOS8+
Edit: I pushed a basic example to github to show my problem: https://github.com/opfeffer/nsurlcache
Check it out and let me know what I'm doing wrong!

I see your GET request has parameters, and for that I think we are facing the same situation.
I reached a solution overriding NSURLCache doing some adjustments and accessing the underlying DB, you can see it here:
https://github.com/JCardenete/NTURLCache/tree/master/NTURLCache
NSURLCache - Disk caching for GET request with parameters not working

Related

How to call REST API deployed in AWS on iOS app?

I am new to AWS and trying to implement a webservice API GET call on iOS app. So following is the postman I have and trying to implement the same in iOS app;
But I am confused in setting this header on URLSession requests. I am not seeing much documentations regarding this. Looking forward for some help in implementing this on iOS app.
Tried to implement the Swift code generated in Postman :
But this implementation returns a Forbidden error message , so i believe some different implementation is needed to connect AWS
Can you show us what you have tried with URLSession so far ?
As said by #burnsi, you show look at the raw headers that are working for you in Postman and try to replicate those.
Some headers should always be used for all requests (given a particular URLSession), so you should consider configuring your session using httpAdditionalHeaders:
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = [
"Accept-Encoding": "application/json",
"Content-Type": "application/json"
]
let session: URLSession = URLSession(configuration: configuration)
For headers that are request-specific or likely to change over time (like an authorization token), you should add them to the request itself using setValue(_:forHTTPHeaderField:):
var request: URLRequest = URLRequest(url: url)
request.setValue("XYZ", forHTTPHeaderField: "Authorization")
Then you should perform the request using:
session.dataTask(with: request, completionHandler: { (data, response, error) in
print("Data: \(data?.debugDescription)\nResponse: \(response?.debugDescription)\nError: \(error?.debugDescription)")
})
Let us know what this code prints out for you and I'll try to help more!

when redirect with code 302, WKWebView cannot set cookie

I sent request to url1. url1 will redirect to url2 with cookie. url2 is for authorization. And I get code "302 found", which is correct.
But when url2 redirect back to url1, the cookie lost. This results my request keeping redirect to url2, looping until failed.
Does any one met this kind of problem and know a solution? Thanks in advance.
This is a WKWebView problem. UIWebView will work fine. But somehow, I need to change to use WKWebView.
I already tried many solutions, such as Can I set the cookies to be used by a WKWebView?. These methods could handle the cookie in same domain. My problem is url1 and url2 have different domains. When redirect, the cookie is missing, which made the authorization failed, and resulted in looping between url1 and url2.
Does anybody met this problem and found out a workaround? Thanks in advance.
Unfortunately, there is no official support for handling 302 redirect response in WKWebView.I came across the same issue too.There is a way to work around but may take some risks and not elegant.
Override the WKWebView.load(_ request: URLRequest) and use URLSession to make the request instead.
Make sure your URLSession can deal with 302 redirect by yourself which means you need to implement func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: #escaping (URLRequest?) -> Void) to specify that you don't want the URLSession to handle 302 redirect automatically.
Once you get the 302 response using URLSession the attached cookie will be set to HTTPCookieStorage.shared automatically.And then make a new request with those cookies and the url of response header field Location by WKWebView itself.
The code sample is Here.I subclass WKWebView and also deal with cookies in the most case including your 302 redirect case.If it works for you, please give me a star thx!
It is a really a hack way and be careful if you want use it in product.
If you need subsequent requests from 'url2' to have their cookies set, you can try using 'WKUserScript' to set the values programmatically via javascript at document start like so:
WKUserContentController* userContentController = WKUserContentController.new;
WKUserScript * cookieScript = [[WKUserScript alloc]
initWithSource: #"document.cookie = 'TeskCookieKey1=TeskCookieValue1';document.cookie = 'TeskCookieKey2=TeskCookieValue2';"
injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
// again, use stringWithFormat: in the above line to inject your values programmatically
[userContentController addUserScript:cookieScript];
WKWebViewConfiguration* webViewConfig = WKWebViewConfiguration.new;
webViewConfig.userContentController = userContentController;
WKWebView * webView = [[WKWebView alloc] initWithFrame:CGRectMake(/*set your values*/) configuration:webViewConfig];
However, if you require your cookies to be set on the initial load request, you can set them on NSMutableURLRequest like so:
WKWebView * webView = /*initialization stuff*/
NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:#"YOUR URL"]];
[request addValue:#"TestCookieKey1=TestCookieValue1;TestCookieKey2=TestCookieValue2;" forHTTPHeaderField:#"Cookie"];
// use stringWithFormat: in the above line to inject your values programmatically
[webView loadRequest:request];
You can combine above two techniques to transfer cookie values from Native environment to WebView environment.
You can refer to this link for more advanced information on cookies.
Maybe late to the party, but I faced something pretty close to the above question last week and also from my side I concluded that for some reason cookies on 302 response are not handled correctly by WKWebView.
If your problem is similar to the one i'm facing with:
you need to pass some cookies from a redirect required for a subsequent authentication stage
Then you can easily interchange your WKWebView with the one I provided into this gist.
and use it like follow:
let myWebView = SAWKWebView(frame: .zero)
...
myWebView.load(aRequest, handleRedirect: true, cookies: [myCookies])
cookies is not required and can be nil, depending on your setup.
SAWKWebView class inheriting from WKWebView and workaround the above problem by using directly URLSession and its delegate.
This way I could rely into a sufficient and safe way to break into the response 302 (aka redirect) and handle cookies manually with HTTPStorageCookie without relying onto javascript and document since in my experience it doesn't always work reliable.
Once the required redirect is performed I render the content as HTML and continue as needed.
Another solution worked for me:
Adopt WKNavigationDelegate, webView.navigationDelegate = self, then:
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
/// Cancel the navigation action
decisionHandler(.cancel)
/// Navigate yourself
let request = NSMutableURLRequest(url: url)
request.allHTTPHeaderFields = HTTPCookie.requestHeaderFields(with: Auth.getAuthCookies())
webView.load(request as URLRequest)
}
This was all that was needed for my simple case, but I imagine others may also need to copy the other request info that's stored in navigationAction.request like the navigationAction.request.allHTTPHeaderFields
I've tried the solutions suggested in the other answers but finally the one that seems to work better in my case was implement this WKNavigationDelegate's method:
func webView(_ webView: WKWebView, didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation!) {
if let url = webView.url {
webView.load(authenticatedRequest(URLRequest(url: url)))
}
}
Where authenticatedRequest() is a private function that given a request, it returns the same request with the headers/cookies/whatever your service needs to authenticate.
Hope someone find this solution helpful.
recently i faced same problem and i fixed this problem by doing this.I am sure cookie lost happen only if you are using iOS < 11, for this you need to apply some work around as i applied recently.
1- pass updated cookies in response from server side.Whenever it changed from server,
Read cookie from response like this way.
if let allHttpHeaders = response.allHeaderFields as? [String: String] {
let cookies = HTTPCookie.cookies(withResponseHeaderFields: allHttpHeaders, for: responseUrl)
for cookie in cookies {
if cookie.name == "cookie name" {
// compare with old cookie value so it will show your cookie is changed now.
if "your_old_cookie_value" != cookie.value {
//update cookie in Script and request header
// reload your page here again.
break
}
}
}
}
This is the minimal change by that you can fix cookie lost issue, for iOS >=11 should not be an issue, for ios 11 and above use httpcookiestore
Let me know if you have any question.
I suppose the proper solution for iOS 11 is to use the WKHTTPCookieStore. It's designed to solve exactly this issue as per this keynote.

How to know if NSURLSessionDataTask response came from cache?

I would like to determine if the response from NSURLSessionDataTask came from cache, or was served from server
I'am creating my NSURLSessionDataTask from
request.cachePolicy = NSURLRequestUseProtocolCachePolicy;
Two easy options come to mind:
Call [[NSURLCache sharedURLCache] cachedResponseForRequest:request] before you make the request, store the cached response, then do that again after you finish receiving data, and compare the two cached responses to see if they are the same.
Make an initial request with the NSURLRequestReturnCacheDataDontLoad cache policy, and if that fails, make a second request with a more sane policy.
The first approach is usually preferable, as the second approach will return data that exists in the cache even if it is stale. However, in some rare cases (e.g. an offline mode), that might be what you want, which is why I mentioned it.
In order to know whether an URLSessionDataTask response comes from the cache or the network, you have to use a custom URLSession and provide it with an URLSessionTaskDelegate which has to implement the following method:
func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics)
You'll find very useful information in metrics, especially the list of transactions metrics for the request. Each transaction metrics has a property resourceFetchType which can either be .localCache, .networklLoad, .serverPush or .unknown.
More information here: Apple documentation regarding URLSessionTaskDelegate
If your information need is only out of curiosity you can view your network usage the Xcode runtime network metics. Change the policy to a different setting and observe the difference.
If you want to use caching then it's not a good answer to disable caching on the client.
if the server has set cache headers (etag, cache-control="no-store") then NSURLSession will revalidate and serve you cached / fresh response based on the 200 / 304 response from the server. However in your code you will always see statusCode 200 regardless of if NSUrlSession received 200 or 304. This is limiting, because you may want to skip parsing, re-creating objects etc when the response hasnt changed.
THe workaround I did, is to use the etag value in the response to determine if something has changed
NSHTTPURLResponse *httpResp = (NSHTTPURLResponse *)response;
NSString *etag = (httpResp && [httpResp isKindOfClass:[NSHTTPURLResponse class]]) ? httpResp.allHeaderFields[#"Etag"] : nil;
BOOL somethingHasChanged = [etag isEqualToString:oldEtag];

dataTaskWithRequest takes too long for a very specific string

I'm creating an IOS app using swift.
Recently, I've encountered a weird bug.
Im trying to check if a url is valid, therefore, I'm creating a request with the url and check for the response. I do this task with dataTaskWithRequest of NSUrlSession.
The weird bug is that if the URL is alibaba ,the response returns after a long time(more than 20 seconds sometimes).
Why does that happen?
As far as i concerned it happens only with this specific url.
Here is some code although its not necessary .
let request = NSMutableURLRequest(URL: validatedUrl)
request.HTTPMethod = "HEAD"
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request){ data, response, error in
// The response here returns after a very long time
let url = request.URL!.absoluteString
I would appreciate some help guys!
You're retrieving the contents of a URL over the Internet. The speed at which this happens is arbitrary. It depends on the speed of the DNS server that looks up the hostname, the speed of the web server that responds to the request, the speed of the user's Internet connection, and the speed of every network in between.
You can safely assume that it will either succeed or time out within three minutes. Twenty seconds isn't even slightly unusual over a cellular network.
You should probably rethink what you're doing with this URL and why you're doing it, or at least try to figure out a way to avoid keeping the user waiting while you fetch the URL.

How do I invalidate iOS's cache for a particular URL?

Using NSURLSession's default caching, how do I invalidate the cache for a particular URL?
I note NSURLCache's removeCachedResponseForRequest: method, but that takes an NSURLRequest object, which I don't have for the original request. Do I need to store those as I create them so I can then pass them back into removeCachedResponseForRequest: or can I just create a new one with the appropriate URL which will then serve as equivalent for the purpose, even if it doesn't have the same header fields and other properties as the original?
If you want to go further you could reset the cached response for the url request you want to force the reload. Doing the following:
let newResponse = NSHTTPURLResponse(URL: urlrequest.URL!, statusCode: 200, HTTPVersion: "1.1", headerFields: ["Cache-Control":"max-age=0"])
let cachedResponse = NSCachedURLResponse(response: newResponse!, data: NSData())
NSURLCache.sharedURLCache().storeCachedResponse(cachedResponse, forRequest: urlrequest)
As the cache-control header of the response hax a max age of 0 (forced) the response will never be returned when you do this request.
Your answer works fine for forcing a single request, but if you want to have two versions of the request one forcing and another relying on the cached response, removing the cached one once you force a request is desired.
The solution turns out not to be invalidating the cache for an existing URL, but to set:
request.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
When you make the next request for the resource you know to be invalid. There are options to ignore the local cache only, or to request that upstream proxies ignore their caches too. See the NSURLRequest/NSMutableURLRequest documentation for details.
Here's what has been working for me:
request.cachePolicy = NSURLRequestCachePolicy.ReloadIgnoringCacheData
Here are all the options listed regarding chache policy, so you may find one that better suits your need:
Using Swift 2.2 and Xcode 7.3

Resources