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.
Related
I'm getting an error while trying to encode a parameter into a request url.
Here is my function to get the request url:
func asURLRequest() throws -> URLRequest {
let url = try baseURL.asURL().appendingPathComponent(path)
var request = URLRequest(url: url)
request.method = method
if method == .get {
request = try URLEncodedFormParameterEncoder().encode(parameters, into: request)
} else if method == .post {
request = try JSONParameterEncoder().encode(parameters, into: request)
request.setValue("application/json", forHTTPHeaderField: "Accept")
}
return request
}
It is working when the parameter is a dictionary like ["id": 1]. The url would be:
http://.../api/v1/items/?id=1
I want to pass the parameter 1 only, so the url would be like this:
http://.../api/v1/items/1
But it doesn't work, I get this error from Alamofire:
requestRetryFailed(retryError:
Alamofire.AFError.requestRetryFailed(retryError:
Alamofire.AFError.parameterEncoderFailed(reason:
Alamofire.AFError.ParameterEncoderFailureReason.encoderFailed(error:
Alamofire.URLEncodedFormEncoder.Error.invalidRootObject("string("1")")))
What you want is a path encoding, not a query encoding or form encoding. There is no specific parameter encoder in Alamofire for path components (though there is an ongoing feature request). Usually people encode them into the path directly, so you can modify your code to do so directly by using a router and having each route encode its own parameters.
func encodeParameters(into request: URLRequest) throws -> URLRequest {
switch self {
case .someRoute(parameters):
return try URLEncodedFormParameterEncoder().encode(parameters, into: request)
case .pathParameterRoute(parameter):
var request = request
request.url?.appendPathComponent(parameter)
return request
}
}
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)
I am trying in the Request Adapter of Alamofire to add a GET parameter. However in the request adapter I am only able to add HTTPHeader fields.
Currently my request adapter looks like:
// MARK: - RequestAdapter
func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
if let url = urlRequest.url, url.lastPathComponent.hasPrefix(baseURLString) {
var urlRequest = urlRequest
// Want to inject param here
// e.g. urlRequest.addParam(param: "session", value: sessionToken")
return urlRequest
}
return urlRequest
}
I have a Router configured for the Paths but since I want my AuthHandler to be responsible to all Authentication related stuff I want to inject my sessionToken. This makes sure, together with RequestRetrier that any HTTP 401 related error is dealt with.
What is the best way to change the urlRequest?
Can you try
let params: Parameters = ["session": sessionToken]
return URLEncoding.default.encode(urlRequest, with: params)
(or)
return URLEncoding.queryString.encode(urlRequest, with: params)
Thanks
Sriram
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.
I've created an NSURLCache subclass which forces caching of responses for a designated amount of time. This is working well, and cached items are expired as expected. However I'm running into issues when trying to forcefully remove a cached response using NSURLCache's removeCachedResponseForRequest: method.
What I'm looking to achieve is to allow users to force an immediate reload of remote data. To do this, I pass an "ignoreCache" flag when making a request. I construct my NSURLRequest as normal, but ask my NSURLCache to remove any previously cached responses for the given request. This doesn't seem to have any effect, however, as the cached result is still present and used when the request is executed.
The documentation around NSURLCache is fairly sparse. The NSURLCache headers state that the NSURLRequest passed to removeCachedResponseForRequest: is used as a key to lookup the associated NSCachedURLResponse object, but there's little information given as to the logistics of that comparison. Does the NSURLCache class expect to receive the same NSURLRequest instance that generated the cached response, or does it simply compare the NSURL it represents?
Hopefully someone can point me in the right direction.
var error: NSError? = nil
var URLRequest: NSMutableURLRequest = self.operationManager.requestSerializer.requestWithMethod("GET", URLString: NSURL(string: request.URL, relativeToURL: self.operationManager.baseURL).absoluteString, parameters: request.parameters, error: &error)
URLRequest.cachePolicy = NSURLRequestCachePolicy.ReturnCacheDataElseLoad
// We've been asked to ignore the cache, so remove our previously cached response
if ignoreCache == true {
NSURLCache.sharedURLCache().removeCachedResponseForRequest(URLRequest)
}
Here's the Swift code from my NSURLCache subclass for reference:
// MARK: - NSURLCache
override func cachedResponseForRequest(request: NSURLRequest!) -> NSCachedURLResponse! {
var cachedResponse: NSCachedURLResponse? = super.cachedResponseForRequest(request)
if(cachedResponse != nil && cachedResponse!.userInfo != nil) {
var cacheDate: NSDate? = cachedResponse!.userInfo![self.cacheExpirationKey] as? NSDate
if(cacheDate != nil) {
var cacheDateLimit: NSDate = cacheDate!.dateByAddingTimeInterval(self.cacheExpirationInterval)
if(cacheDate!.compare(NSDate()) == NSComparisonResult.OrderedAscending) {
self.removeCachedResponseForRequest(request)
} else {
return cachedResponse
}
}
}
// Either our cached data was too old, or we don't have any that match the NSURLRequest
return nil
}
override func storeCachedResponse(cachedResponse: NSCachedURLResponse!, forRequest request: NSURLRequest!) {
var userInfo: NSMutableDictionary = NSMutableDictionary(dictionary: cachedResponse.userInfo)
userInfo[cacheExpirationKey] = NSDate().dateByAddingTimeInterval(self.cacheExpirationInterval)
var modifiedCacheResponse: NSCachedURLResponse = NSCachedURLResponse(response: cachedResponse.response, data: cachedResponse.data, userInfo: userInfo, storagePolicy: cachedResponse.storagePolicy)
super.storeCachedResponse(modifiedCacheResponse, forRequest: request)
}
Assuming you are on iOS 8, removeCachedResponseForRequest: does not work.
See http://blog.airsource.co.uk/2014/10/13/nsurlcache-ios8-broken-2/