Objective-C – ASIHTTPRequest caches even if new content is available - objective-c

I'm using ASIHTTPRequest to download data from the internet. It has a nifty cacheing feature that you can turn on so that it caches the downloaded data. So far so good. But when I upload new data to my webserver and try to download it again I'm expecting it not to use the cache since the data is new and modified. But even so it will still use the cache.
I'm using the following code for my request:
[ASIHTTPRequest setDefaultCache:[ASIDownloadCache sharedCache]];
// When you turn shouldRespectCacheControlHeaders off, the cache will store responses even if the server
// has explictly asked for them not be be cached (eg with a cache-control or pragma: no-cache header)
[[ASIDownloadCache sharedCache] setShouldRespectCacheControlHeaders:NO];
NSURL *officesUrl = [NSURL URLWithString:#"http://www.example.com/example.json"];
ASIHTTPRequest *officesRequest = [ASIHTTPRequest requestWithURL:officesUrl];
[officesRequest setDefaultResponseEncoding:NSUTF8StringEncoding];
[officesRequest addRequestHeader:#"Cache-Control" value:#"no-cache"];
// Always ask the server if there is new content available,
// If the request fails, use data from the cache even if it should have expired.
[officesRequest setCachePolicy:ASIAskServerIfModifiedCachePolicy|ASIFallbackToCacheIfLoadFailsCachePolicy];
[officesRequest setCacheStoragePolicy:ASICachePermanentlyCacheStoragePolicy];
[officesRequest setDelegate:self];
[officesRequest startAsynchronous];
EDIT:
Two images to show the different headers from HTTPScoop. First image is a fresh download with no cached content. Second image I have uploaded new and modified data.
First:
Second:
EDIT2: Added logs
https://gist.github.com/1139351

I was expecting way more debug output than that, I'm not sure why you got so little.
I think you are just going to have to step through the response processing and see why it decides the cached version is okay to use. The code's pretty easy to follow.
Try setting a breakpoint on useDataFromCache in ASIHTTPRequest.m to start with and canUseCachedDataForRequest, particularly when called from readResponseHeaders.

Related

ASIHTTPRequest seems to cache JSON data always

I'm using ASIHTTPRequest API to get some JSON data from a web side. I'm using an asynchronous request without changing default cache properties. Code is as follows:
__unsafe_unretained ASIHTTPRequest * request = [ASIHTTPRequest requestWithURL:url];
request.timeOutSeconds = 30;
[request setCompletionBlock:^(void)
{
NSString *str = [request responseString];
/*
*
*
*/
}];
[request setFailedBlock:^(void)
{
NSLog(#"status: %#",[request responseStatusMessage]);
NSLog(#"%# : %#",[request url],[[request error] debugDescription]);
}];
[request startAsynchronous];
However, obtained JSON data remains as the old one although content of JSON data in server changes.
I checked data using web browser, both on laptop and on iPhone safari. When i request url after a change in JSON, it first returns old data, but if i refresh the page, it returns updated data. But in the app, ASIHTTPRequest always returns the old data.
I also try to debug ASIHTTPRequest code in order to see whether any cached data used. But it seems like it never uses download cache because it has not been set. It never enters [useDataFromCache] method.
What could be the problem? How can i force ASIHTTPRequest to check whether there is an updated data on server, and make it get the true updated JSON?
EDIT
I used Cache-Control header, and now i get the correct updated JSON data. Code is as follows:
[request addRequestHeader:#"Cache-Control" value:#"no-cache"];
However, i think from now on request will always try to retrieve JSON even if it is not modified, which will decrease performance.
How can i make it first check the server whether data is modified, and retrieve if it is modified? Currently i get JSON data as a dynamic response from a php url, so there is no file which i can check up to dateness of the data.
What could be the solution?
Regards,
Given everything you've said, it seems unlikely that ASIHTTPRequest is cacheing the response.
So, something else must be - it seems like you have a cacheing proxy server inbetween you and the server, and that's why setting Cache-Control makes a difference.
It could be a proxy server on your local network, or it could be at your ISP, or it could be in front of the web server you're using.
According to ASIHTTPRequest's documentation, calling the method
[[ASIDownloadCache sharedCache] clearCachedResponsesForStoragePolicy:ASICachePermanentlyCacheStoragePolicy];
will clear the cache. Call this method before you send the request and it should give you the updated JSON data.

Clear cookies for a given iOS App

My App connects to a server and based on a cookie the server will issue a different response.
Is it no possible to programmatically clear the cookie store, so that the server will not recognize my App when it contacts the server the next time.
I gathered that clearing the Cookies in the Settings.app does only apply for cookies within Safari.
Thanks very much for your comment.
Okay... following up on my earlier comment (and hoping this is the solution you are looking for), you probably want to utilize:
[[NSHTTPCookieStorage sharedHTTPCookieStorage] deleteCookie:]
for each of the cookies for your site.
If you want your changes to the NSHTTPCookieStorage to be retained, you'll also want to call off to
[[NSUserDefaults standardUserDefaults] synchronize];
To prevent this from slowing down your app, you may also want to call this on a background thread like so:
dispatch_queue_t backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_async(backgroundQueue, ^{
//TODO: Cookie deletion logic here
});
EDIT:
If you just need to disregard cookies altogether for a given NSURLRequest, you can do so with:
[request setHTTPShouldHandleCookies:NO];
Where request is your instance of NSURLRequest.
As mentioned by #Niralp it isn't possible to delete all cookies by passing nil to deleteCookie: on an instance of NSHTTPStorage. However, since iOS 8 there has been a removeFromDate: method that can be utilised to the same effect.
In Swift 4 this would be:
HTTPCookieStorage.shared.removeCookies(since: Date(timeIntervalSince1970: 0))
That would remove all cookies in the app since the 1970 epoch which is likely suitable for most needs.

Why is NSData crashing my app?

I have a url that is a link to an audio file and will be played using AVFoundation.framework. But for some reason, when the app reaches setting the NSData it crashes. Please help.
NSURL *url = [NSURL URLWithString:songPathForm];
NSData *soundData = [NSData dataWithContentsOfURL:url];
EDIT::::
This is what I did to make it stop crashing, but the data contains nothing
NSString *url = [songPathForm stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
//NSURL *url = [NSURL URLWithString:songPathForm];
NSURLRequest *songRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:url]
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:60.0];
NSURLConnection *songConnection = [[NSURLConnection alloc] initWithRequest:songRequest delegate:self];
if(songConnection)
{
songData = [[NSMutableData data] retain];
}
It comes out as <>
There is absolutely no way to answer this question as it is worded, but here are some clues:
if there is a crash, there is a backtrace. Post it.
if there is a crash, there is some kind of an error. Post it.
Given that code, there are two failure modes that I can think of:
songPathForm is nil or corrupt and/or not an URL.
the data at the URL is too large to download and causes the app to attempt to allocate a HUGE amount of memory (there are cases where an allocation can be large enough to crash an app w/o the system jetsam mechanism kicking in).
There is no crash message, it just freezes and stops responding. url
is not nil it is...
Then why did you say your app crashed?!
dataWithContentsOfURL: is synchronously downloading the contents of whatever is at the URL.
Thus, you are blocking the main event loop during the download and that is why your app is not responsive.
You need to asynchronously download the data; i.e. not block the main event loop.
However that probably won't entirely fix your problem as it looks like the contents of that URL is really large and, thus, you are likely going to run out of memory if you try to download in memory.
You either need to download the file to the disk or you need to download only parts of it that you need right now or you need to stream it (if it really is a large audio file as the URL implies).
The code in your updated question doesn't make any sense. How do you expect songData to be filled with data from the connection?
When doing stuff asynchronously, you are basically saying "go do this stuff and let me know every now and then how it is going". In this case, that'd be a notification that more data is available or that the connection is done reading (or in error).
You can't ask for the data immediately because the data isn't immediately available.
You'll want to read through this guide.

Can't reauthenticate with different NSURLCredentials (old deleted ones are used)

i've been searching stackoverflow, google, apple and other places. The tips provided look promising, i implemented them but alltogether don't seem to work or get enforced.
Problem: I have an NSURLConnection with specific credentials. I then have a logout where I clear the credentials, the protectionspace, i remove all cached responses and delete all cookies in the sharedHTTPCookieStorage but when calling my authenticated request again a few seconds later even with wrong credentials I still am using the old (deleted) credentials
Here are some code extracts, where credentials are removed
NSDictionary *credentialsDict = [[NSURLCredentialStorage sharedCredentialStorage] allCredentials];
if ([credentialsDict count] > 0) {
// the credentialsDict has NSURLProtectionSpace objs as keys and dicts of userName => NSURLCredential
NSEnumerator *protectionSpaceEnumerator = [credentialsDict keyEnumerator];
id urlProtectionSpace;
// iterate over all NSURLProtectionSpaces
while (urlProtectionSpace = [protectionSpaceEnumerator nextObject]) {
NSEnumerator *userNameEnumerator = [[credentialsDict objectForKey:urlProtectionSpace] keyEnumerator];
id userName;
// iterate over all usernames for this protectionspace, which are the keys for the actual NSURLCredentials
while (userName = [userNameEnumerator nextObject]) {
NSURLCredential *cred = [[credentialsDict objectForKey:urlProtectionSpace] objectForKey:userName];
WriteLog(#"Method: switchView removing credential %#",[cred user]);
[[NSURLCredentialStorage sharedCredentialStorage] removeCredential:cred forProtectionSpace:urlProtectionSpace];
}
}
}
I then remove all cached responses
NSURLCache *sharedCache = [NSURLCache sharedURLCache];
[sharedCache removeAllCachedResponses];
I then delete all cookies
NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
NSArray *cookies = [cookieStorage cookies];
for (NSHTTPCookie *cookie in cookies) {
[cookieStorage deleteCookie:cookie];
NSLog(#"deleted cookie");
}
I also tried using no cookies and other policies
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:theURL cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:60.0];
[request setHTTPShouldHandleCookies:NO];
if(self.currentCookies != nil){
[request setAllHTTPHeaderFields:
[NSHTTPCookie requestHeaderFieldsWithCookies:nil]];
}
theConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
I also tried this hint here on specifically storing the cookies and passing them again. http://www.hanspinckaers.com/multiple-nsurlrequests-with-different-cookies. There's another blog on the web suggesting to add a "#" to each URL in order to enforce reauthentication, which works but just does not solve the issue because I need to count on session's credentials and the ability to use totally different credentials.
Is this a bug or known issue and how do I really solve this...
Put bluntly: What am I exactly doing wrong here?
This is really bugging me and keeping me from continuing my work.
I would greatly appreciate any input!
Thanks alot!
Unfortunately, there does not seem to be a solution to this problem.
You can use NSURLCredentialPersistenceNone or the # trick or you can define the 'connectionShouldUseCredentialStorage' delegate method to return NO. If you do that every time and your app never persists the credentials for a session, that will force the challenge to occur on every request.
For apps that are only performing minimal requests or that end up using a session cookie for authentication, that might work OK.
For apps that are sending a large number of requests, these solutions all result in a 401 response for every single request and that extra challenge-response can add up in terms of data and performance.
It would be nice if you could persist the credential storage for the session until you needed to log out and then switch to one of the work-arounds, but that is not possible.
As soon as you store the credentials once for a session, they get cached for the entire TLS session. That results in a need to wait about 10 minutes until that session goes away.
You can read more about this issue at: http://developer.apple.com/library/ios/qa/qa1727/_index.html
That document mentions a limited work-around that involves appending a '.' to the end of the server name. I have been unable to get that working, however.
Other than that, these are the solutions I can think of:
1) Always use the NSURLCredentialPersistenceNone & connectionShouldUseCredentialStorage workaround that should generate the 401s. Add the Basic authentication header to the request, yourself. This should prevent the extra 401's while also bypassing the credential storage. The code for adding that authorization looks something like this:
NSString *s ;
NSString *authStr ;
s = [NSString stringWithFormat:#"%#:%#",user,password] ;
s = [YourBase64Encoder base64EncodingForData:[NSData dataWithBytes:[s UTF8String] length:strlen([s UTF8String])]];
authStr = [NSString stringWithFormat:#"Basic %#",s] ;
[request setValue:authStr forHTTPHeaderField:#"Authorization"] ;
I don't know how this would be implemented for other authentication methods, but I presume it is possible.
2) Inform the user of the issue and ask them to restart the app
3) Implement your own low-level sockets based http retrieval mechanism that bypasses CFNetwork completely. Good luck with that :>)
I just ran into this issue with AFNetworking. I'm working with a backend that requires Authorization to be passed in the header. However, when the user logs out of the app and attempts to log back in (even with different creds) I was getting an error from the server. My solution was to clear out my apps cookies when clearing the authheader in logout.
- (void)clearAuthorizationHeader {
[self.manager.requestSerializer clearAuthorizationHeader];
NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (NSHTTPCookie *cookie in [storage cookies]) {
[storage deleteCookie:cookie];
}
}
I've run into this issue too. Clearing NSURLCredentialStorage seems to partially work, but it seems like I have to wait a few seconds after this for it to take effect. Doing another HTTP request without waiting results in the old Authorization header being used.
I was able to fix it by passing NSURLCredentialPersistenceNone while initializing my NSURLCredential:
NSURLCredential* credentials = [[NSURLCredential alloc] initWithUser:username password:password persistence:NSURLCredentialPersistenceNone];
note: this will cause 401 Challenges on every HTTP request you make with this NSURLCredential. However this isn't an issue if you get back some cookies that keep you authenticated.
For what it's worth, I'm having the same problem.
I think it's a timing issue. When using the simulator, if I wait 5-10 seconds before trying to log in again, login fails as expected. Also, when I use an actual phone, I can rarely get the problem to occur - which might be a function of the phone being slower, or might be a bug in the simulator.
I know it's an old topic. However the only thing, that works for me was to use different urls for different certificates.
It worked in my application, since I have only 2 certificates (one general in application resources and one custom downloaded from internet after the user verification process). In my case there are no cookies, and no credentials to clear so none of the solutions I found on stackoverflow worked.
I was facing the same problem, now it works.
Using NSURLConnection this issue can be fixed easily by adding a random number to the end of the URL:
So, search for your URLRequest and append the random number to URLRequest
NSInteger randomNumber = arc4random() % 999;
NSString *requestURL = [NSString stringWithFormat:#"%#?cache=%ld",yourURL,(long)randomNumber];
NSURL *URLRequest = [NSURL URLWithString:requestURL];
And make sure you have a random number at the end of all URLs you are calling.

iOS: Asynchronously Download Images from Web *unless* they already exists in filesystem?

In my iOS app I'm using Core Data to model a number of entities that exist in a remote Ruby on Rails application. A number of these entities reference the URLs of images stored in S3 which I need to download and store locally on the user's iDevice as needed.
My specific requirements are:
Display the image (in a UITableView usually or on its own) from the local filesystem.
IF the image doesn't exist locally I need to be able to download it in the background (usually there will be two images that need to be downloaded ... a thumbnail and an original). A default image should be displayed until the downloaded image is persisted.
Once the image is downloaded I need to have it saved in the filesystem and displayed on the screen.
I know there are a number of related posts but I'm interested to hear of what you all would consider a recommended approach based on my specific needs. Also, are there any gotchas I need to be wary of?
Thanks -wg
Use ASIHTTPRequest. Read the section on using a download cache here - http://allseeing-i.com/ASIHTTPRequest/How-to-use
Basically you'll want to turn on the cache which you can do for all requests or just your image requests:
[ASIHTTPRequest setDefaultCache:[ASIDownloadCache sharedCache]]; // all requests
// specific requests
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setDownloadCache:[ASIDownloadCache sharedCache]];
Then you probably want to use the cache policy:
ASIOnlyLoadIfNotCachedCachePolicy
and the storage policy:
ASICachePermanentlyCacheStoragePolicy
However multiple cache policies would meet your needs.
I'm recommending to use ASI because it solves lots of problems in an app. I recently developed my own network library, partially as an exercise and partially because I did not know about ASI and didn't like the other options I found at the time. I recently started moving everything to use ASI and I'm very happy with my choice
I have a similar requirement for a project I worked on. In my cases I wanted local files when running in test mode but remote files otherwise.
To implement this, I defined a "ResourceURLProvider" protocol within the header file for the class that would be loading the files:
#protocol ResourceURLProvider
-(NSURL *)getURLForFile:(NSString *fileName);
#end
#interface MyFileLoader {
id<ResourceURLProvider> provider;
}
#property (nonatomic, assign) id<ResourceURLProvider> provider;
In my implementation class I did something like this:
NSURL *url = [provider getURLForFile:#"myfile.xml"];
if (!url) {
url = // Some code to create the URL for the remote myfile.xml
}
If you set something up like this then you can implement your version of the provider (whatever you name it) and have it returned a URL to the cached file if it exists. If not, you return nil and the code above will use the URL to the remote location.
Unlike my case, you want to cache the file after you've downloaded it. To do that I might add another method to the protocol I defined:
#protocol ResourceURLProvider
-(NSURL *)getURLForFile:(NSString *fileName);
-(void)cacheFileData:(NSData *)data filename:(NSString *)fileName;
#end
Your implementation would look at the file name and check to see if the file was already cached. If not, the NSData (the contents of the file) would be written to your cache.
Hope that helps.