Search code examples
iosobjective-cnsurlcachensurlprotocol

How to use NSURLCache to cache content served by an NSURLProtocol


I've written an NSURLProtocol that will check outbound http requests against a plist of URL to local path mappings and serve up the local content instead, and then cache it using NSURLCache:

- (void)startLoading
{   
    //Could this be why my responses never come out of the cache?
    NSURLResponse *response =[[NSURLResponse alloc]initWithURL:self.request.URL
                                                      MIMEType:nil expectedContentLength:-1
                                              textEncodingName:nil];

    //Get the locally stored data for this request
    NSData* data = [[ELALocalPathSubstitutionService singleton] getLocallyStoredDataForRequest:self.request];

    //Tell the connection to cache the response
    [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];

    //Have the connection load the data we just fetched
    [[self client] URLProtocol:self didLoadData:data];

    //Tell the connection to finish up
    [[self client] URLProtocolDidFinishLoading:self];
}

I limit the number of times local data can be fetched to one. The intent that the first time that its fetched it'll come from the NSBundle, but thereafter it'll use the stock NSURLCache to check whether it should come from either the cache or the network:

+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
    //Check if we have pre-loaded data for that request
    ELAPathSubstitution* pathSub = [[ELALocalPathSubstitutionService singleton] pathSubForRequest:request];

    //We don't have a mapping for this URL
    if (!pathSub)
        return NO;

    //If it's been fetched too many times, don't handle it
    if ([pathSub.timesLocalDataFetched intValue] > 0)
    {
        //Record that we refused it.
        [pathSub addHistoryItem:ELAPathSubstitutionHistoryRefusedByProtocol];
        return NO;
    }

    //Record that we handled it.
    [pathSub addHistoryItem:ELAPathSubstitutionHistoryHandledByProtocol];
    return YES;
}

Sadly, it seems as though the local data will go into the cache, but it won't ever come back out. Here's a log snippet:

History of [https://example.com/image.png]:
[2014-04-29 18:01:53 +0000] = [ELAPathSubstitutionHistoryHandledByProtocol]
[2014-04-29 18:01:53 +0000] = [ELAPathSubstitutionHistoryHandledByProtocol]
[2014-04-29 18:01:53 +0000] = [ELAPathSubstitutionHistoryHandledByProtocol]
[2014-04-29 18:01:53 +0000] = [ELAPathSubstitutionHistoryCacheMiss]
[2014-04-29 18:01:53 +0000] = [ELAPathSubstitutionHistoryDataFetched]
[2014-04-29 18:01:53 +0000] = [ELAPathSubstitutionHistoryAddedToCache]
[2014-04-29 18:02:11 +0000] = [ELAPathSubstitutionHistoryRefusedByProtocol]
[2014-04-29 18:02:11 +0000] = [ELAPathSubstitutionHistoryRefusedByProtocol]

[2014-04-29 18:02:11 +0000] = [ELAPathSubstitutionHistoryCacheMiss] 
[2014-04-29 18:02:11 +0000] = [ELAPathSubstitutionHistoryAddedToCache]
[2014-04-29 18:02:50 +0000] = [ELAPathSubstitutionHistoryRefusedByProtocol]
[2014-04-29 18:02:50 +0000] = [ELAPathSubstitutionHistoryCacheHit]

My expectation is that after the first time it's refused by the protocol it'll result in a couple of cache hits but instead it always counts it as a miss, fetches the content from the server, and then after that I start getting cache hits.

My fear is that my NSURLProtocol subclass constructs its responses in a way that allows them to be cached, but prevents them from ever being pulled out of the cache. Any ideas?

Thanks in advance. :)


Solution

  • Interacting with the URL loading system's cache is a responsibility of the NSURLProtocolClient object that is acting as the client of the NSURLProtocol. If the request is using NSURLRequestUseProtocolCachePolicy as the cache policy, it's up to the protocol implementation to apply the correct protocol-specific rules to determine wether a response should be cached or not.

    The protocol implementation, at whatever point(s) are appropriate for the protocol, calls URLProtocol:cachedResponseIsValid: on it's client, indicating that the cached response is valid. The client should be then interacting with the URL loading system's caching layer.

    However, since the client that the system provides us is private and opaque you may want to take matters into your own hands and interact with the system cache within your protocol. If you want to take that path, you can use the NSURLCache directly. The first step is to override -cachedResponse in your protocol. If you read the documentation carefully, the default implementation only sets this from the value passed into the initializer. Override it so that it accesses the shared URL cache (or your own private URL cache):

    - (NSCachedURLResponse *) cachedResponse {
        return [[NSURLCache sharedURLCache] cachedResponseForRequest:[self request]];
    }
    

    Now in the places where you would normally call cachedResponseIsValid: on the client, also store an NSCachedURLResponse into the NSURLCache. For example, when you have a complete set of bytes and a response:

    [[NSURLCache sharedURLCache] storeCachedResponse:cachedResponse forRequest:[self request]];