Dan Wood: The Eponymous Weblog (Archives)

Dan Wood Dan Wood is co-owner of Karelia Software, creating programs for the Macintosh computer. He is the father of two kids, lives in the Bay Area of California USA, and prefers bicycles to cars. This site is his older weblog, which mostly covers geeky topics like Macs and Mac Programming. Go visit the current blog here.

Useful Tidbits and Egotistical Musings from Dan Wood

Categories: Business · Mac OS X · Cocoa Programming · General · All Categories

Wed, 28 Feb 2007

File Caching, take 2: using NSURLCache directly

Recently I blogged about a home-spun cache technique. After some discussion in the comments, Robert Blum sent me a snippet of code which uses NSURLCache and related classes. I had no idea these classes could really be used for anything outside of the URL loading system, so I explored them a bit further, and with the help of some other code I found on CocoaBuilder, I got something working.

I've defined two category methods on NSURLCache. My code calls cachedDataForPath: to look and see if the data have already been cached; if not, it is generated and then saved for next time using cacheData:forPath:. Note that this code generates a file:// URL; I'll explain why after the code.

See more ...

Q: When is an connection error not an error?

A: When it's a status code.

If you are using NSURLConnection for accessing data over HTTP, as I like to do from time to time, you may find yourself scratching your head wondering why you didn't receive the connection:didFailWithError: callback in your delegate when clearly there was an error retrieving the resource you were looking for.

The reason is that connections that return an error as its status code (such as a 404 "Not Found" or 500 "Internal Server Error") don't result in a failure callback.

What you need to do is check for status codes in your implementation of connection:didReceiveResponse:. If you are fetching a resource over HTTP, then your NSURLResponse in the callback will actually be NSHTTPURLResponse, and you can query the status code. Any value greater than or equal to 400 is generally an error.

My general pattern is to cancel the connection, package up an NSError object based on that status code, and call the connection:didFailWithError: callback myself. This way I can treat status-code errors the same as I would another kind of error. (I have appropriated the string constant NSHTTPPropertyStatusCodeKey, normally used by NSURLHandle, as the error domain string.)

if ([response respondsToSelector:@selector(statusCode)])
{
  int statusCode = [((NSHTTPURLResponse *)response) statusCode];
  if (statusCode >= 400)
  {
    [connection cancel];  // stop connecting; no more delegate messages
    NSDictionary *errorInfo
      = [NSDictionary dictionaryWithObject:[NSString stringWithFormat:
          NSLocalizedString(@"Server returned status code %d",@""),
            statusCode]
                                    forKey:NSLocalizedDescriptionKey];
    NSError *statusError
      = [NSError errorWithDomain:NSHTTPPropertyStatusCodeKey
                            code:statusCode
                        userInfo:errorInfo];
    [self connection:connection didFailWithError:statusError];
  }
}

Side note: Don't forget that you may recieve the connection:didReceiveResponse: callback multiple times, in the case of redirects!