NSURLCache using cache for requests older than max-age - ios

I've been banging my head against the while now trying to figure out what's going on with NSURLCache.
Basically, the server I am connecting to doesn't set any cache control headers... So following various guides and apple docs (ie https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/URLLoadingSystem/Concepts/CachePolicies.html) I set my own cache control headers in the willCacheResponse delegate and then return that modified response in the completion handler. The apple docs and the resource I've read seem to indicate this should work. But what I'm seeing is that the cached data is returned after it should be expired based on max-age. It seems like max-age is being ignored and the NSURLCache is using another heuristic to determine if it should pull data from the cache or not.
I set the max-age=60 cache-control header and I've verified using Charles that the data is pulled from cache and no network requests are made long after 60 seconds. Eventually (seems non-deterministic) a new request will be made that actually goes to the server (usually after a few hours have passed and I try the request again).
Here is the code, for testing purposes I'm just hardcoding the max-age to 60 seconds:
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, completionHandler: #escaping (CachedURLResponse?) -> Void) {
var modifiedReponse: URLResponse? = nil
if let HTTPResponse = proposedResponse.response as? HTTPURLResponse {
if var newHeaders = HTTPResponse.allHeaderFields as? [String : String] {
if newHeaders["Cache-Control"] == nil {
newHeaders["Cache-Control"] = "max-age=60"
}
modifiedReponse = HTTPURLResponse(url: HTTPResponse.url!, statusCode: HTTPResponse.statusCode, httpVersion: "HTTP/1.1", headerFields: newHeaders)
}
}
let response = modifiedReponse ?? proposedResponse.response
var newCachedResponse: CachedURLResponse? = nil
newCachedResponse = CachedURLResponse(response: response, data: proposedResponse.data, storagePolicy: proposedResponse.storagePolicy)
}
There are a few optional checks in there but I have confirmed that the response on newCachedResponse I am returning has the cache control header set to max-age=60. Am I doing something obviously wrong here? Or is NSURLCache just F'd?

I know this is super late, but I think you just need to pass that newCachedResponse back into the completion handler closure provided to you.
Such as the last line being:
completionHandler(newCachedResponse)

Related

URLRequest cookies are always empty

When trying to retrieve the cookies from anywhere I can't parse the cookies. The storage seems to be always empty. Maybe I'm just fundamentally misunderstanding the whole thing.
I start off by doing a get or post request against my endpoint via a shared URLSession task (for example: https://httpbin.com/cookies)
If I get a valid response status I try to parse the cookies (Note that response is not nil and everything works fine here, I just didn't include that code because i don't think its relevant):
func setCookies(response: URLResponse) {
let response = response as! HTTPURLResponse
let cookies = HTTPCookie.cookies(withResponseHeaderFields:response.allHeaderFields as! [String : String], for: response.url!)
print("cookies: \(cookies) \(HTTPCookieStorage.shared.cookies!)")
}
For both of these methods my result is always empty.
Even though the header fields are set:
I already did set cookieAcceptPolicy to .alwaysand httpShouldHandleCookies to true
Is there something else I'm missing? How is it possible for both of them to be empty?

NSURLCache Alamofire removing cached responses

I'm using the following code to test a behavior in NSURLCache. I initialize an API instance in AppDelegate. I configure the manager according to Alamofire's documentation, I configure the shared cache, and I assign dataTaskWillCacheResponse to make sure that the response will indeed be cached.
Then I call makeRequest which checks if a cached response exists (which it shouldn't on the first launch) and then I use my manager to make a request using the same URL so that the request is equivalent throughout the test.
My breakpoint at dataTaskWillCacheResponse is hit, I continue, the responseJSON block is executed and is Successful so I performTests using the request.
First, I check if the response is cached. It is: good!
Second, (and this is the problem) I remove the cached response for that request and then check if it exists. It does: bad!
Third, I check if removing all cached responses will remove that response. It does: good! But it's odd that that worked and the previous attempt at just removing the single response didn't...
Here's the code:
import Alamofire
class API: Manager.SessionDelegate {
var manager: Manager!
override init() {
super.init()
manager = Manager(session: urlSession(), delegate: self)
configureCache(memoryCapacityMB: 5, diskCapacityMB: 25)
manager.delegate.dataTaskWillCacheResponse = { urlSession, dataTask, cachedResponse in
// Placing a breakpoint here confirms that the response is going to be cached
return cachedResponse
}
}
private func urlSession() -> NSURLSession {
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
return NSURLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}
private func configureCache(memoryCapacityMB memory: Int, diskCapacityMB disk: Int) {
let memoryCapacity = memory * 1024 * 1024
let diskCapacity = disk * 1024 * 1024
let sharedCache = NSURLCache(memoryCapacity: memoryCapacity, diskCapacity: diskCapacity, diskPath: nil)
NSURLCache.setSharedURLCache(sharedCache)
}
// MARK: Request
func makeRequest() {
// The response should be nil on the first launch since nothing has been cached
let request = NSURLRequest(URL: NSURL(string: "http://jsonplaceholder.typicode.com/posts")!)
let response = NSURLCache.sharedURLCache().cachedResponseForRequest(request)
print(response)
manager.request(.GET, request.URLString).responseJSON { response in
switch response.result {
case .Success:
self.performTests(with: response.request!)
case .Failure:
break
}
}
}
func performTests(with request: NSURLRequest) {
// Should exist
var response = NSURLCache.sharedURLCache().cachedResponseForRequest(request)
print(response)
// And it does: good!
// Remove the cached resopnse and check if it exists
NSURLCache.sharedURLCache().removeCachedResponseForRequest(request)
response = NSURLCache.sharedURLCache().cachedResponseForRequest(request)
print(response)
// And it does: bad!
// Try removing all cached responses and check if it exists
NSURLCache.sharedURLCache().removeAllCachedResponses()
response = NSURLCache.sharedURLCache().cachedResponseForRequest(request)
print(response)
// And it doesn't: good! But odd...
}
}
So how does one remove the cached response of a single request then? And is this unintended behavior? Or is NSURLCache behaving correctly and I'm just missing something? Thanks ahead of time for taking a look!
My recollection is that most URL cache changes are not synchronous. They only actually happen after you return to the run loop and allow various asynchronous callbacks to occur.
Try running the rest of the code asynchronously after a delay of 3-5 seconds and see if the request has been removed.
If that doesn't fix the problem, file a bug.

NSURLCache with changing Mashery "sig=XXXXXX" query item

Is it possible to use NSURLCache to cache responses when the URL includes a changing query item? For example, we add Mashery's required "sig=XXXXXX" query item, which changes for each request.
If not, is there a workaround?
Solved by subclassing NSURLCache and overriding its caching methods.
In each overridden method, I remove the query item from the request prior to calling the superclass' method.
For example:
override func storeCachedResponse(cachedResponse: NSCachedURLResponse, forRequest request: NSURLRequest) {
let strippedRequest = removeQueryItemFromRequest(self.queryItemName, request: request)
if let url = strippedRequest.URL {
let response = NSURLResponse(URL: url, MIMEType: cachedResponse.response.MIMEType, expectedContentLength: Int(cachedResponse.response.expectedContentLength), textEncodingName: cachedResponse.response.textEncodingName)
let newCachedResponse = NSCachedURLResponse(response: response, data: cachedResponse.data)
super.storeCachedResponse(newCachedResponse, forRequest: strippedRequest)
}
else {
super.storeCachedResponse(cachedResponse, forRequest: request)
}
}
self.queryItemName is a stored property passed in to a custom initializer.

Authenticated http request swift Alamofire

I'm struggling with getting this to work to make request to my API. Without a token works, but when I try to add additional headers, things turn to be complicated, for me.
First, the structure.
one class called: APIAsyncTask that makes the requests
one class called APIParams, just a data holder to send parameters to the APIAsyncTask class.
one class called DatabaseAPI that makes that builds the parameters, and send that to the APIAsyncTask class.
DatabaseAPI
func someMethod()
{
let task = APIAsyncTasks()
task.registerCallback { (error, result) -> Void in
print("Finished task, back at DatabaseAPI")
}
let params2 = APIParams(request: .GET, apiPath: "Posts/1", apiToken: "4iTX-56w")
task.APIrequest(params2)
}
APIAsyncTask
This part is for fixing another error, because manager was not global, the task got cancelled quickly.
var manager : Manager!
init(authenticatedRequest : Bool, token: String?)
{
manager = Alamofire.Manager()
print("Pre \(manager.session.configuration.HTTPAdditionalHeaders?.count)")
if(authenticatedRequest && token != nil)
{
var defaultHeaders = Alamofire.Manager.sharedInstance.session.configuration.HTTPAdditionalHeaders!
defaultHeaders["Authorization"] = "bearer \(token)"
let configuration = Manager.sharedInstance.session.configuration
configuration.HTTPAdditionalHeaders = defaultHeaders
manager = Alamofire.Manager(configuration: configuration)
}
print("Post \(manager.session.configuration.HTTPAdditionalHeaders?.count)")
}
After some decision making, it comes down to this part.
private func GetRequest(url: String!,token : String?, completionHandler: (JSON?, NSURLRequest?, NSHTTPURLResponse?, NSError?) -> () ) -> ()
{
print("Begin Get Request")
if(token != nil)//if token is not nil, make authenticated request
{
print("just before request: \(manager.session.configuration.HTTPAdditionalHeaders?.count)")
manager.request(.GET, url, parameters: nil, encoding: .JSON).responseJSON { (request, response, json, error) in
print("Get Request (authenticated), inside alamofire request")
var resultJson : JSON?
if(json != nil)
{
resultJson = JSON(json!)
}
completionHandler(resultJson, request, response, error)
}
}
else
{
//working part without token
So as the code is now, I get an error on completing:
Mattt himself gives the answer of using Alamofire.Manager.sharedInstance.session.configuration.HTTPAdditionalHeaders
, so that should be fine...
I suspect it has something to do with the multiple threads, according to this blog. Or, since it is something about CFNetwork, it could be because my API does not use SSL? I disabled NSAppTransportSecurity
I'm kind of new to swift, so examples would be really appreciated! Thankyou!
So the majority of your code looks solid.
The error leads me to believe that CFNetwork is having difficulty figuring out how to compute the protection space for the challenge. I would also assume you are getting a basic auth challenge since you are attaching an Authorization header.
Digging through your logic a bit more with this in mind led me to see that your not attaching your token to the string properly inside the Authorization header. You need to do the following instead.
defaultHeaders["Authorization"] = "bearer \(token!)"
Otherwise your Authorization header value is going to include Optional(value) instead of just value.
That's the only issue I can see at the moment. If you could give that a try and comment back that would be great. I'll update my answer accordingly if that doesn't actually solve your problem.
Best of luck!
You can add your headers in your request with Alamofire 2 and Swift 2.
For an example: go to example

save session in http request swift

in my app I'm using JSON and I made a session recently so if I would like to make some http request to get data for a specific user, the user must log in before (also used by http request).
in the safari when I entering the url's of login and then the url of receive data, it does that as needed.
but in my app, I first call login and then the url for getting data, but it's probably starting a new session in every url request which leads me to get an error and not receive the data.
my url request function is:
static func urlRequest (adress: String, sessionEnded: (NSDictionary->Void)?){
println(adress)
var urli = NSURL(string: adress)
var request = NSURLRequest(URL: urli!)
var rVal = "";
self.task = NSURLSession.sharedSession().dataTaskWithURL(urli!) {(data, response, error) in
var parseError: NSError?
let parsedObject: AnyObject? = NSJSONSerialization.JSONObjectWithData(data,
options: NSJSONReadingOptions.AllowFragments,
error:&parseError)
let po = parsedObject as NSDictionary
if let a = sessionEnded{
sessionEnded!(po)
}
}
task!.resume()
}
thanks in advance!!
You have shared only half of the puzzle with us, the client code. We can't comment on why the app isn't working with a clearer picture of what the server API. For example, once you "log in", how do subsequent queries confirm that the request is coming from valid session. Furthermore, you report that "every url request which leads me to get an error". Well, what error do you receive? You have to be far more specific regarding the precise errors/crashes you are receiving. BTW, are you logging on to some service with a well-defined API or are you writing that code yourself, too?
Having said that, I might suggest a few refinements to this method:
The sessionEnded (which I've renamed completionHandler to conform to informal standard naming conventions), probably should return an optional NSError object, too, so the caller can detect if there was an error.
Your unwrapping of the sessionEnded completion handler can be simplified to use ?.
When you parse the object, you should feel free to perform the optional cast, too.
You probably want to detect a network error (in which case data would be nil) and return the network NSError object.
Minor point, but I'd probably also rename the function to conform to Cocoa naming conventions, using a verb to start the name. Perhaps something like performURLRequest.
This is your call, but I'd be inclined to have the method return the NSURLSessionTask, so that the caller could use that task object if it wanted to (e.g. save the task object so that it could cancel it later if it wanted to).
Thus, that yields something like:
func performURLRequest (address: String, completionHandler: ((NSDictionary!, NSError!) -> Void)?) -> NSURLSessionTask {
let url = NSURL(string: address)
let task = NSURLSession.sharedSession().dataTaskWithURL(url!) {(data, response, error) in
if data == nil {
sessionEnded?(nil, error)
} else {
var parseError: NSError?
let parsedObject = NSJSONSerialization.JSONObjectWithData(data, options: nil, error:&parseError) as? NSDictionary
completionHandler?(parsedObject, parseError)
}
}
task.resume()
return task
}
And you'd invoke it like:
performURLRequest("http://www.example.com/some/path") { responseDictionary, error in
if responseDictionary == nil {
// handle error, e.g.
println(error)
return
}
// use `responseDictionary` here
}

Resources