Alamofire: New cache policy - AlwaysCacheAndHitNetworkIfExpired

Created on 5 Oct 2016  路  1Comment  路  Source: Alamofire/Alamofire

I'd like to write adapter for Alamofire which let me always use Cached data even when it's expired, and in case it's expired call remote and update cache?

Is this case very rare so Apple didn't include it in their policies, or it just doesn't make sense?

question url cache

Most helpful comment

Hi @badeleux,

This is really an awesome question. Let me take a stab at walking you through how you could accomplish this.

Problem

There's no such policy provided by Apple because it ultimately results in two requests which the URLSession APIs just aren't designed to do. However, you can definitely accomplish this behavior, but not the way you are intending.

By default, Apple is going to cache the result of a data request in most cases. I'd suggest reading through the docs on the URLSessionDataDelegate protocol to get familiar with the willCacheResponse behavior. In almost all cases, Apple and Alamofire will cache the response in the shared URL cache.

Solution

Now I've thought through quite a few different ways to do this, and the best approach I could come up with was the following:

let request = Alamofire.request("https://httpbin.org/get").responseJSON { response in
    let data = response.data
    // Use data to do whatever you need
}

if let urlRequest = request.request {
    let cachedResponse = URLCache.shared.cachedResponse(for: urlRequest)
    let data = cachedResponse.data
    // Use data to do whatever you need
}

This logic isn't quite perfect, but it's close. Let's walk through the three success case scenarios that exist here.

Scenario 1 (no cached response)

The network request is sent with the default cache policy meaning it will first try to load from the cache if not expired, otherwise it will go hit the network. In the event that there's no cached response, the cache will be checked and it will not return a response, so the request will head to the network to download the response. The subsequent check then to extract the cached response while the network request is in-flight will fail since there's no cached response. Then, once the network request succeeds, the response is cached and you can use the data returned by the server. Yay!

Scenario 2 (expired cached response)

Now the logic is a bit different when the data is cached and expired. The network request will find the cached response, but ignore it since it's expired and will head to the network to get a fresh copy. The URL request is then used to load the cached response from the URL cache while the network request is in flight. Thus you use the expired data while the request is inflight, then replace it once the network request succeeds. Yay!

Senario 3 (valid cached response)

The final scenario is that the response is cached and is not expired. Now what's going to happen here is that the network request is going to load the cached response since it isn't expired. But before it is returned, you will actually fall into the URL request logic that directly fetches the cached response and uses it because the network request has to hop a few different queues before it can call your completion closure. Once the hops are complete (nano-milliseconds), your cached response is returned for the network request which is the same cached response you just loaded. You could parse the response twice and use it, or you could add some state tracking comparing the previous data against the new data to make sure they're different. Either way, you have to add your own state tracking there which is a bit of a bummer, but it can be done pretty easily.


Summary

Hopefully that helps shed some light on what's all going on there. You could always implement your own logic to determine whether the cached response has actually expired, but that's going to be really difficult to do while matching the exact same behavior that Apple uses. It's unfortunately that Apple doesn't expose an expired Bool on a CachedURLResponse. It would certainly make this type of issue much easier.

Again, remember this is just one approach. There are MANY different ways you could solve this problem, but I think this is probably the simplest.

Cheers. 馃嵒

>All comments

Hi @badeleux,

This is really an awesome question. Let me take a stab at walking you through how you could accomplish this.

Problem

There's no such policy provided by Apple because it ultimately results in two requests which the URLSession APIs just aren't designed to do. However, you can definitely accomplish this behavior, but not the way you are intending.

By default, Apple is going to cache the result of a data request in most cases. I'd suggest reading through the docs on the URLSessionDataDelegate protocol to get familiar with the willCacheResponse behavior. In almost all cases, Apple and Alamofire will cache the response in the shared URL cache.

Solution

Now I've thought through quite a few different ways to do this, and the best approach I could come up with was the following:

let request = Alamofire.request("https://httpbin.org/get").responseJSON { response in
    let data = response.data
    // Use data to do whatever you need
}

if let urlRequest = request.request {
    let cachedResponse = URLCache.shared.cachedResponse(for: urlRequest)
    let data = cachedResponse.data
    // Use data to do whatever you need
}

This logic isn't quite perfect, but it's close. Let's walk through the three success case scenarios that exist here.

Scenario 1 (no cached response)

The network request is sent with the default cache policy meaning it will first try to load from the cache if not expired, otherwise it will go hit the network. In the event that there's no cached response, the cache will be checked and it will not return a response, so the request will head to the network to download the response. The subsequent check then to extract the cached response while the network request is in-flight will fail since there's no cached response. Then, once the network request succeeds, the response is cached and you can use the data returned by the server. Yay!

Scenario 2 (expired cached response)

Now the logic is a bit different when the data is cached and expired. The network request will find the cached response, but ignore it since it's expired and will head to the network to get a fresh copy. The URL request is then used to load the cached response from the URL cache while the network request is in flight. Thus you use the expired data while the request is inflight, then replace it once the network request succeeds. Yay!

Senario 3 (valid cached response)

The final scenario is that the response is cached and is not expired. Now what's going to happen here is that the network request is going to load the cached response since it isn't expired. But before it is returned, you will actually fall into the URL request logic that directly fetches the cached response and uses it because the network request has to hop a few different queues before it can call your completion closure. Once the hops are complete (nano-milliseconds), your cached response is returned for the network request which is the same cached response you just loaded. You could parse the response twice and use it, or you could add some state tracking comparing the previous data against the new data to make sure they're different. Either way, you have to add your own state tracking there which is a bit of a bummer, but it can be done pretty easily.


Summary

Hopefully that helps shed some light on what's all going on there. You could always implement your own logic to determine whether the cached response has actually expired, but that's going to be really difficult to do while matching the exact same behavior that Apple uses. It's unfortunately that Apple doesn't expose an expired Bool on a CachedURLResponse. It would certainly make this type of issue much easier.

Again, remember this is just one approach. There are MANY different ways you could solve this problem, but I think this is probably the simplest.

Cheers. 馃嵒

Was this page helpful?
0 / 5 - 0 ratings