I am trying to enable eTag support in my app.
I am using Alamofire 4 in my swift 3 project.
It seems that eTag is transparently handled by URLRequest which Alamofire uses:
NSURLCache and ETags
But it doesn't work.
Here is http header sent by web server:
headers {
Connection = "keep-alive";
"Content-Length" = 47152;
"Content-Type" = "application/json";
Date = "Tue, 06 Dec 2016 22:43:18 GMT";
Etag = "\"ecf38288be2f23f6710cafeb1f344b8c\"";
} })
Do yo have any hint?
Thanks a lot.
By default, caching is ON. If you log HTTP traffic inside your app, you might see cached responses, without the app making requests to server this time.
In case when URLSession decides to return cached response instead of going to server you will see same Date HTTP response header.
To ensure caching is working, you should inspect network packets between your app and server.
This is the default cache policy when using an URLRequest.
If you want to change this behavior and see the "real" response from the network, so that you can handle the eTag and the statusCode by yourself, you just need to change the cache policy to reloadIgnoringCacheData:
var exampleRequest = try! URLRequest(url: route, method: .post, headers: headers)
// This is the important line.
exampleRequest?.cachePolicy = .reloadIgnoringCacheData
Hope this helps someone!
Related
I'm using Alamofire 5 and have the requirement that some GET-requests should be cached. If the data is older then 20 minutes the real API should be hit.
What I found is to use the ResponseCacher. But I do not see a way to configure the individual request and need some advice.
let responseCacher = ResponseCacher(behavior: .modify { _, response in
let userInfo = ["date": Date()]
return CachedURLResponse(
response: response.response,
data: response.data,
userInfo: userInfo,
storagePolicy: .allowed)
})
let configuration = URLSessionConfiguration.af.default
private override init() {
configuration.timeoutIntervalForRequest = 10
configuration.requestCachePolicy = .reloadRevalidatingCacheData
Session(
configuration: configuration,
serverTrustManager: ServerTrustManager(evaluators: evaluators),
cachedResponseHandler: responseCacher
)
If the backend is returning proper caching headers that you want to limit to a certain amount of time, adding a Cache-Control: max-age= header on the request may work.
If the backend isn't return proper caching headers, using ResponseCacher is the way to go. You would modify the CachedURLResponse's response to include the proper Cache-Control header.
To elaborate on Jon's answer, the easiest way to achieve what you want is to just let the backend declare the cache semantics of this endpoint, then ensure that on the client side, URLSession uses a URLCache (which is probably the default anyway) and let URLSession and the backend do the rest. This requires, that you have control over the backend, though!
The more elaborate answer:
Here is just an example, how a server may return a response with declared cache semantics:
URL: https://www.example.com/ Status Code: 200
Age: 238645
Cache-Control: max-age=604800
Date: Tue, 12 Jan 2021 18:43:58 GMT
Etag: "3147526947"
Expires: Tue, 19 Jan 2021 18:43:58 GMT
Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT
Vary: Accept-Encoding
x-cache: HIT
Accept-Ranges: bytes
Content-Encoding: gzip
Content-Length: 648
Content-Type: text/html; charset=UTF-8
Server: ECS (dcb/7EC7)
This server literally outputs the full range of what a server can declare regarding caching. The first eight headers (from Age to x-cache) declare the caching.
Cache-Control: max-age=604800 for example declares, that the data's freshness equals 604800 seconds. Having the date when the server created the data, the client can now check if the data is still "fresh".
Expires: Tue, 19 Jan 2021 18:43:58 GMT means the exact same thing, it declares when the data is outdated specifying the wall clock. This is redundant with the above declaration, but it is very clearly defined in the HTTP how clients should treat this.
Having an Age header is a hint, that the response has been actually delivered from a cache that exists between the client and the origin server. The age is the estimation of this data's age - the duration from when it has been created on the origin and when it has been delivered.
I don't wont to go into detail what every header means exactly and how a client and server should behave according HTTP since this is a very intense topic, but what you have to do is basically when you define an endpoint, is just to define the duration of the "freshness" of the returned data.
The whole details: Hypertext Transfer Protocol (HTTP/1.1): Caching
Once you came up with a good duration, Web-application frameworks (like Rails, SpringBoot, etc.) give great help with declaring cache semantics out of the box. Then Web-application frameworks will output the corresponding headers in the response - more or less "automagically".
The URLSession will automatically do the right thing according the HTTP protocol (well, almost). That is, it will store the response in the cache and when you perform a subsequent request it first looks for a suitable response in the cache and return that response if the "freshness" of the data is still given.
If that cached data is too old (according the given response headers and the current data and time), it will try to get a fresh data by sending the request to the origin server. Any upstream cache or eventually the origin server may then return fresh data. Your URLSession data task does all this transparently without giving you a clue whether the data comes from the cache or the origin server. And honestly, in most cases you don't need to know it.
Declaring the cache semantics according HTTP is very powerful and it usually should suit your needs. Also, the client may tailor its needs with specifying certain request headers, for example allowing to return even outdated data or ignoring any cached values, and much more.
Every detail may deserve a dedicated question and answer on SO.
Using the URLSession family of classes, is there a way to check the validity of a response? Specifically, I have an HTTP response whose Cache-Control header specifies no-cache, so that any cached response will have to be submitted for validation before it can be used. I can retrieve the CachedURLResponse object from URLCache.shared, but none of URLSession, URLCache, or CachedURLResponse seem to have any methods for determining whether such a cached response is still valid. Such methods are also absent from URLSessionDelegate and URLSessionTaskDelegate.
Is there any way to do this other than initiating the actual validation request myself? Presumably this is done somewhere in the URLSession stack (although I'm not sure of this), but it looks as if this functionality may just not be exposed by the public API.
The question is why do you need to check the validity. If you want only to check the validity you may use a probably uncommon method by using URLProtocol and writing a custom NSURLProtocolClient. The client has only empty methods except:
func urlProtocol(_ protocol: URLProtocol, cachedResponseIsValid cachedResponse: CachedURLResponse) {
// Cached response is valid. Store this information in a appropriate way.
protocol.stopLoading()
}
Now, you create a protocol with that client
let myClient = ...
let protocol = NSURLProtocol(request, cachedResponse, client: myClient)
protocol.startLoading()
protocol.stopLoading() // Stop, if urlProtocol(_: cachedResponseIsValid:) was not called
As far as I know, the caching of an HTTP response is handled automatically. (Just to be sure, I tried it again just before answering this question).
The default caching policy on a URLRequest is useProtocolCachePolicy. So, on all the subsequent requests to that url, the HTTP headers will contain the If-None-Match key, with the latest Etag as its value.
If this is not working for you automatically, make sure that the server is sending the Etag header in its response. Also, that the server acknowledges the If-None-Match header on all the requests.
TL;DR
CHECK:
That you get the Cache-Control and the Etag header in your
responses from the server.
That the server receives an If-None-Match header with the
correct Etag value, in all the subsequent requests.
That the server is actually configured to handle the cached
responses (As in, it responds to the If-None-Match header
correctly).
References:
https://developer.apple.com/documentation/foundation/nsurlrequest.cachepolicy
https://devcenter.heroku.com/articles/ios-network-caching-http-headers
I am able to see cached responses when I query NSURLCache directly but when I request the same resource through NSURLSession:dataTaskWithRequest it always queries the server and never gives me the cached response, even with internet disabled.
I configure NSURLCache in application:didFinishLaunchingWithOptions like this:
let URLCache = NSURLCache(memoryCapacity: 20 * 1024 * 1024,
diskCapacity: 80 * 1024 * 1024, diskPath: nil)
NSURLCache.setSharedURLCache(URLCache)
Then I am using this code to check the cache and fetch the response:
print("cached response is \(NSURLCache.sharedURLCache().cachedResponseForRequest(request)?.response)")
NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
print("nsurlsession response \(response)")
}.resume()
The result of my debug prints are:
cached response is Optional(<NSHTTPURLResponse: 0x7f809d0ddfe0> { URL: https://api.test.com/stuff } { status code: 200, headers {
"Cache-Control" = "public, s-maxage=600";
Connection = "keep-alive";
"Content-Type" = "application/json";
Date = "Tue, 06 Sep 2016 23:41:24 GMT";
Etag = "\"4800a836fee27c56a3cce1f0f2bddaefa5a7785b\"";
Server = "nginx/1.11.3";
"Transfer-Encoding" = Identity;
"X-RateLimit-Limit" = 60;
"X-RateLimit-Remaining" = 2; } })
nsurlsession response Optional(<NSHTTPURLResponse: 0x7f809d202e50> { URL: https://api.test.com/stuff } { status code: 200, headers {
"Cache-Control" = "public, s-maxage=600";
Connection = "keep-alive";
"Content-Type" = "application/json";
Date = "Tue, 06 Sep 2016 23:51:52 GMT";
Etag = "\"4800a836fee27c56a3cce1f0f2bddaefa5a7785b\"";
Server = "nginx/1.11.3";
"Transfer-Encoding" = Identity;
"X-RateLimit-Limit" = 60;
"X-RateLimit-Remaining" = 52; } })
As you can see, the Cache-Control headers from the server are set to allow caching. I am not doing anything special with my request, just creating a default NSURLRequest with a URL. Each time I trigger the request, the new response gets cached, but it is never retrieved on subsequent requests.
Is there any reason why NSURLSession would not use a response stored in NSURLCache? Is there something I must do to tell NSURLSession to actually look up requests in the cache?
I can't tell you with absolute certainty why the cache isn't being consulted, but I can give you a list of the most likely reasons:
The server did not respond with 304 when queried about the validity of that ETag header (IIRC using a HEAD request).
The request is too big—either relative to the size of the buffer or in absolute terms. The cache should at least be a couple of orders of magnitude bigger than the requests that you would typically cache; anything over about 5% of the cache size will not be cached.
The request method is something other than GET. (Only GET requests are cached unless you monkey with the machinery significantly.)
More than 10 minutes have elapsed (600 seconds isn't very long).
The request was made in a different URL session that has a different backing cache.
The request was made in an ephemeral URL session or a session that for some other reason has no cache.
The session actually is returning the cached response, but you're seeing a request because it is revalidating a little more aggressively than you might expect—possibly because it will reach its maximum age so soon.
Your URL request is getting handled in the background by a custom NSURLProtocol that doesn't respect the cache (e.g. because of some badly behaved third-party networking or advertising framework).
The request had not actually been fully written to the cache when you tried to retrieve it (timing race caused by multiple threads).
I'm probably forgetting several others. With that said, if I'm forgetting them, that probably means that they aren't documented.
So...
If you verify that everything listed above is working as expected, file a bug at bugreporter.apple.com and include enough code to reproduce the problem, along with a packet dump if possible.
I'm working on an app using backend server I have no control over. Server response have headers
"Cache-Control" = "max-age=0, private, no-store, no-cache, must-revalidate";
Expires = "Fri, 20 Jun 2014 09:00:39 GMT";
among others, but both these values are wrong, the response should be cached for 15mins. I've searched and found what seems to be a solution to this, i.e. here Bypassing http response header Cache-Control: how to set cache expiration?
But it's not working for me, - storeCachedResponse: forRequest: is never being called. I'm also setting NSURLRequestReturnCacheDataElseLoad on a request.
Any idea on what I might be doing wrong would be greatly appreciated!
I am using AFNetworking 2 for an iOS project consuming a REST API. When requests fail, I am not able to get the body response.
I have seen this SO answer that says it can be retrieved from the userInfo dictionary. Unfortunately in my case I am not getting the NSLocalizedRecoverySuggestion key-value in my userInfo dictionary with the response body. Instead I see a AFNetworkingOperationFailingURLResponseErrorKey key.
My NSError log is
Error Domain=AFNetworkingErrorDomain Code=-1011 "Request failed: bad request (400)" UserInfo=0xbe471a0 {NSErrorFailingURLKey=http://***.com/api/users, AFNetworkingOperationFailingURLResponseErrorKey=<NSHTTPURLResponse: 0xbc60e80> { URL: http://***.com/api/users } { status code: 400, headers {
Connection = "keep-alive";
"Content-Length" = 85;
"Content-Type" = "application/json; charset=utf-8";
Date = "Thu, 24 Apr 2014 21:36:57 GMT";
"X-Powered-By" = Express;
} }, NSLocalizedDescription=Request failed: bad request (400)}
As you can see from the headers, I am getting "Content-Type" = "application/json; charset=utf-8"; so this other answer doesn't apply either.
Actually the body response in Postman looks like this:
{"errorCode":100,"description":"There is already a registered user with that email."}
The backend is implemented by myself in Node + Express. I am not very experienced in backend development though, so maybe there is something I am missing or that I could change.
Does anyone know why I am not getting the response body in userInfo?
Quoting AFNetworking owner:
(...) the recommended approach is to create a custom response serializer that translates response data in failing cases into NSError objects as appropriate.
You can see this answer on another AFNetworking issue to see how to integrate it with your session manager, and this repository to see how to implement it.