I'm using EVURLCache and the POST request response is cached even if cache-control and Pragma headers are defined :
Headers: [
Access-Control-Allow-Headers : Content-Type, Accept, X-Requested-With, remember-me, x-auth-token
Content-Type : application/json;charset=UTF-8
Access-Control-Max-Age : 3600
Cache-Control : no-cache, no-store, max-age=0, must-revalidate
Server : nginx/1.9.15
Connection : keep-alive
Transfer-Encoding : Identity
X-XSS-Protection : 1; mode=block
X-Content-Type-Options : nosniff
Expires : 0
X-Application-Context : application:dev,docker:8080
Pragma : no-cache
Access-Control-Allow-Methods : POST, GET, OPTIONS, DELETE, PUT
Date : Thu, 18 Aug 2016 13:57:15 GMT
Access-Control-Allow-Origin : *
Access-Control-Allow-Credentials : true
]
How to force EVURLCache not to cache such a request ?
I'm initializing cache in my app delegate using the following code:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// configuring cache
EVURLCache.LOGGING = true // We want to see all caching actions
EVURLCache.activate()
return true
}
Note that I know filters but I wonder if we can just tell it to follow the response header suggested cache behavior
I have created an update (not yet pushed to GitHub) and would like to have your input. The code for the fix for this is:
var shouldSkipCache: String? = nil
// check if caching is allowed according to the request
if request.cachePolicy == NSURLRequestCachePolicy.ReloadIgnoringCacheData {
shouldSkipCache = "request cache policy"
}
// check if caching is allowed according to the response Cache-Control or Pragma header
if let httpResponse = cachedResponse.response as? NSHTTPURLResponse {
if let cacheControl = httpResponse.allHeaderFields["Cache-Control"] as? String {
if cacheControl.lowercaseString.containsString("no-cache") || cacheControl.lowercaseString.containsString("no-store") {
shouldSkipCache = "response cache control"
}
}
if let cacheControl = httpResponse.allHeaderFields["Pragma"] as? String {
if cacheControl.lowercaseString.containsString("no-cache") {
shouldSkipCache = "response pragma"
}
}
}
if shouldSkipCache != nil {
// If the file is in the PreCache folder, then we do want to save a copy in case we are without internet connection
let storagePath = EVURLCache.storagePathForRequest(request, rootPath: EVURLCache._preCacheDirectory) ?? ""
if !NSFileManager.defaultManager().fileExistsAtPath(storagePath) {
EVURLCache.debugLog("CACHE not storing file, it's not allowed by the \(shouldSkipCache) : \(request.URL)")
return
}
EVURLCache.debugLog("CACHE file in PreCache folder, overriding \(shouldSkipCache) : \(request.URL)")
}
In short this means that:
If the request was created while indicating that cached data should beignored then the response will not be written to the cache
If the response has a Cache-Control header that contains no-cache or no-store then the response will not be written to the cache
If the response has a Pragma header that contains no-cache then the response will not be written to the cache
All above will be ignored if the file is already in the cache. (then at least it should be updated)
Related
I want to create an iOS Application that logs into a website and parses the data from several pages of that site, while maintaining the login session.This is what I have done so far. I send a GET request to retrieve the EVENTVALIDATON and VIEWSTATE parameters required for the POST request. (I looked at the POST by using 'Firebug'). When I run the following code, it gives back the same login page. But it should be giving me this page.
var parameter: Parameters = [:]
var viewstate: String = ""
var eventvalidation: String =
#IBAction func postRequest(_ sender: Any) {
Alamofire.request("https://ecampus.psgtech.ac.in/studzone/AttWfLoginPage.aspx").responseString { response in
print("\(response.result.isSuccess)")
if let html = response.result.value {
if let doc = Kanna.HTML(html: html, encoding: String.Encoding.utf8) {
// Search for nodes by CSS selector
for show in doc.css("input[id='__VIEWSTATE']") {
self.viewstate=show["value"]!
//print(show["value"] as Any)
}
for show in doc.css("input[id='__EVENTVALIDATION']") {
self.eventvalidation=show["value"]!
//print(show["value"] as Any)
}
}
}
//creating dictionary for parameters
self.parameter = ["__EVENTTARGET":"",
"__EVENTARGUMENT":"",
"__LASTFOCUS":"",
"__VIEWSTATE":self.viewstate,
"__EVENTVALIDATION":self.eventvalidation,
"rdolst":"S",
"Txtstudid":"<myrollno>",
"TxtPasswd":"<mypassword>",
"btnlogin":"Login"
]
}
Alamofire.request ("https://ecampus.psgtech.ac.in/studzone/AttWfLoginPage.aspx",method: .post, parameters: self.parameter, headers: headers).responseString { response in
print("\(response.result.isSuccess)")
print(response)
}
To be honest, I'm very new to requests and parsing data(I have finished the parsing part separately though). I did some more research and read about headers and cookies.So after checking the headers, the initial GET request by the browser has a response header of
Cache-Control : private
Content-Encoding : gzip
Content-Length : 4992
Content-Type : text/html; charset=utf-8
Date : Sun, 18 Jun 2017 14:25:50 GMT
Server : Microsoft-IIS/8.0
Set-Cookie : .ASPXAUTH=; expires=Mon, 11-Oct-1999 18:30:00 GMT; path=/; HttpOnly
Vary : Accept-Encoding
X-AspNet-Version : 4.0.30319
X-Powered-By : ASP.NET
and Request Header of
Accept : text/html,application/xhtml+xml,application/xml;q=0.9;q=0.8
Accept-Encoding : gzip, deflate, br
Accept-Language : en-US,en;q=0.5
Connection : keep-alive
Cookie : ASP.NET_SessionId=urzugt0zliwkmz3ab1fxx1ja
Host : ecampus.psgtech.ac.in
Upgrade-Insecure-Requests : 1
User-Agent : Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:54.0) Gecko/20100101 Firefox/54.0`
The problem is I don't understand how a initial GET request can have a token with it. If request happens first, response should be the one containing the token? I don't know what I am doing wrong and how to get this working. I don't know if I am missing something altogether. I came here only after trying everything I could think of. Any help would be appreciated. Thank you.
EVENTVALIDATON and VIEWSTATE parameters required for the POST <--
But in your code the POST request is executed immediately after the GET request, at this point the self.parameter is empty
Alamofire has asynchronous completionHandler
Wait for the GET request to complete, and then send the POST request:
var parameter: Parameters = [:]
var viewstate: String = ""
var eventvalidation: String =
#IBAction func postRequest(_ sender: Any) {
Alamofire.request("https://ecampus.psgtech.ac.in/studzone/AttWfLoginPage.aspx").responseString { response in
print("\(response.result.isSuccess)")
if let html = response.result.value {
if let doc = Kanna.HTML(html: html, encoding: String.Encoding.utf8) {
// Search for nodes by CSS selector
for show in doc.css("input[id='__VIEWSTATE']") {
self.viewstate=show["value"]!
//print(show["value"] as Any)
}
for show in doc.css("input[id='__EVENTVALIDATION']") {
self.eventvalidation=show["value"]!
//print(show["value"] as Any)
}
}
}
//creating dictionary for parameters
self.parameter = ["__EVENTTARGET":"",
"__EVENTARGUMENT":"",
"__LASTFOCUS":"",
"__VIEWSTATE":self.viewstate,
"__EVENTVALIDATION":self.eventvalidation,
"rdolst":"S",
"Txtstudid":"15i231",
"TxtPasswd":"OpenSesame",
"btnlogin":"Login"
]
//Wait for the GET request to complete, and then send the POST request: <<==
Alamofire.request ("https://ecampus.psgtech.ac.in/studzone/AttWfLoginPage.aspx",method: .post, parameters: self.parameter, headers: headers).responseString { response in
print("\(response.result.isSuccess)")
print(response)
}
}
Is there a way to detect 304 Not Modified response with Alamofire 4? I find that Alamofire response.statusCode is always 200 even if server responded with 304.
Network call setup:
Alamofire
.request("http://domain.com/api/path", method: .get)
.validate(statusCode: 200..<300)
.validate(contentType: ["application/json"])
.responseJSON { response in
print(response.response?.statusCode)
}
Alamofire response header
<NSHTTPURLResponse: 0x61800003e1c0> { URL: http://domain.com/api/path } { status code: 200, headers {
"Access-Control-Allow-Headers" = "content-type, authorization";
"Access-Control-Allow-Methods" = "GET, PUT, POST, DELETE, HEAD, OPTIONS";
"Access-Control-Allow-Origin" = "*";
"Cache-Control" = "private, must-revalidate";
Connection = "keep-alive";
"Content-Type" = "application/json";
Date = "Mon, 23 Jan 2017 23:35:00 GMT";
Etag = "\"f641...cbb6\"";
"Proxy-Connection" = "Keep-alive";
Server = "nginx/1.10.1";
"Transfer-Encoding" = Identity;
} }
Server response
HTTP/1.1 304 Not Modified
Server: nginx/1.10.1
Connection: keep-alive
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: content-type, authorization
Access-Control-Allow-Methods: GET, PUT, POST, DELETE, HEAD, OPTIONS
Cache-Control: private, must-revalidate
ETag: "f641...cbb6"
Date: Mon, 23 Jan 2017 23:35:00 GMT
Since NSURLSessions default behavior is to abstract from cached 304 responses by always returning 200 responses (but not actually reloading the data), I first had to change the cachingPolicy as follows:
urlRequest.cachePolicy = .reloadIgnoringCacheData
Then, I adjusted the status codes of the validate function to accept 3xx responses and handled the codes accordingly:
Alamofire.request("https://httpbin.org/get")
.validate(statusCode: 200..<400)
.responseData { response in
switch response.response?.statusCode {
case 304?:
print("304 Not Modified")
default:
switch response.result {
case .success:
print("200 Success")
case .failure:
print ("Error")
}
}
}
You can use manual response validation with Alamofire as outlined here.
Alamofire.request("https://httpbin.org/get")
.validate(statusCode: 200..<300)
.validate(contentType: ["application/json"])
.responseData { response in
switch response.result {
case .success:
print("Validation Successful")
case .failure(let error):
print(error)
}
}
I have a file in my webserver and I am downloading that to my app everytime I access it because its possible that file content might be changed But If it is changed I would like to download that time only so bandwidth can be saved and fortunately that's what this ETag and If-None-Match header fields are for.
When I make a request first time ,I retrieve the ETag from the HTTP response headers
In the subsequent requests to download that file I'd attach the Etag value for If-None-Match headerfield so that if there is no change then I'd get HTTP response status code 304 or else I'd get 200 if there is a change in the file.
Note:
When I try the above steps in Advanced REST Client Application in chrome it works fine as it is supposed to be but when I try that in iOS I always get the response code 200 but it should have given me 304 for the subsequent requests.
Here is the sample code I use
var request1 = NSMutableURLRequest(URL:NSURL(string: "http://10.12.1.101/Etag/ringtone1.mp3")!)
let Etagvalue="\"36170-52c1cc36d9b40\""
var session1 = NSURLSession.sharedSession()
request1.HTTPMethod = "GET"
var err: NSError?
request1.addValue(Etagvalue, forHTTPHeaderField: "If-None-Match")
var task = session1.dataTaskWithRequest(request1, completionHandler: {data, response, error -> Void in
print("response: \(response)")
})
Here is the response
response: Optional( { URL:
http://10.12.1.101/Etag/ringtone1.mp3 } { status code: 200, headers {
"Accept-Ranges" = bytes;
Connection = "Keep-Alive";
"Content-Length" = 221552;
"Content-Type" = "audio/mpeg";
Date = "Wed, 24 Feb 2016 14:57:53 GMT";
Etag = "\"36170-52c1cc36d9b40\"";
"Keep-Alive" = "timeout=5, max=100";
"Last-Modified" = "Fri, 19 Feb 2016 10:15:33 GMT";
Server = "Apache/2.4.16 (Unix) PHP/5.5.29"; } })
What am I doing wrong here ?
I've encountered the same problem. I've discovered that it's because of cachePolicy. You need to set it as follows:
request.cachePolicy = .ReloadIgnoringLocalAndRemoteCacheData
And you will be OK
CachePolicy naming convention is confusing and better yet it actually does not implement some of them...
This article explains them well. http://nshipster.com/nsurlcache/
Also, if you let the Cache Policy to use UseProtocolCachePolicy then your NSURLSession will receive Status code 200 with response generated from Cache.
This issue is due to the cache policy 'useProtocolCachePolicy', 'returnCacheDataElseLoad' or 'returnCacheDataDontLoad'.
You can use any other policy then above one. Preferably 'reloadIgnoringLocalAndRemoteCacheData'.
.reloadRevalidatingCacheData was introduced recently which sounds a bit more efficient than the reloadIgnoringLocalAndRemoteCacheData way of the yore
Environment: Swift 2, iOS 9
I had rolled my own HTTP networking library. It wasn't broken but I decided to fix it and switch to Alamofire.
Here is my code snippet:
case .HTTPTokenAuth: // Token Auth
let dictionary = Locksmith.loadDataForUserAccount("Auth_Token", inService: "KeyChainService")
let token = dictionary!.valueForKey("access_token")
var aManager = Manager.sharedInstance
aManager.session.configuration.HTTPAdditionalHeaders = ["Authorization": "Bearer \(token!)" ]
aManager.request(method, requestURL!, parameters: parameters, encoding: .JSON).response { request, response, data, error in
print("===== Request ================")
print(request!.allHTTPHeaderFields!)
print("===== Response ================")
print(response!.allHeaderFields)
print("===== Error ================")
print(error)
}
The problem is that for some reason the Oauth token is not making it into the header. (At least that is what appears to be the problem as I don't see it in the Request header. I receive the following request/response:
===== Request ================
["Content-Type": "application/json"]
===== Response ================
[Content-Type: application/json,
Www-Authenticate: Bearer realm="Doorkeeper",
error="invalid_token",
error_description="The access token is invalid",
Pragma: no-cache, X-Runtime: 0.002205,
X-Powered-By: Phusion Passenger 4.0.14, X-XSS-Protection: 1; mode=block, Server: nginx/1.6.2 + Phusion Passenger 4.0.14,
Transfer-Encoding: Identity,
Cache-Control: no-store,
Date: Tue, 01 Sep 2015 18:54:16 GMT,
X-Request-Id: 395d2154-5054-423c-a7bc-f7ef85e0cdbf,
Connection: keep-alive,
X-Content-Type-Options: nosniff,
X-UA-Compatible: chrome=1,
Status: 401 Unauthorized,
X-Frame-Options: SAMEORIGIN]
I went with the simpler approach:
case .HTTPTokenAuth: // Token Auth
let dictionary = Locksmith.loadDataForUserAccount("Auth_Token", inService: "KeyChainService")
let token = dictionary!.valueForKey("access_token")
let headers = ["Authorization":"Bearer \(token!)"]
Alamofire.request(method, requestURL!, parameters: parameters, headers: headers, encoding: .JSON)
.response { request, response, data, error in
print(request!.allHTTPHeaderFields!)
print(response)
print(error)
}
I'm using NSURLSession to request a JSON resource from an HTTP server. The server uses Cache-Control to limit the time the resource is cached on clients.
This works great, but I'd also like to cache a deserialized JSON object in memory as it is accessed quite often, while continuing to leverage the HTTP caching mechanisms built into NSURLSession.
I'm thinking I can save a few HTTP response headers: Content-MD5, Etag, and Last-Modified along with the deserialized JSON object (I'm using those 3 fields since I've noticed not all HTTP servers return Content-MD5, otherwise that'd be sufficient by itself). The next time I receive a response for the JSON object, if those 3 fields are the same then I can reuse the previously deserialized JSON object.
Is this a robust way to determine the deserizlied JSON is still valid. If not, how do I determine if the deserialized object is up to date?
I created a HTTPEntityFingerprint structure which stores some of the entity headers: Content-MD5, Etag, and Last-Modified.
import Foundation
struct HTTPEntityFingerprint {
let contentMD5 : String?
let etag : String?
let lastModified : String?
}
extension HTTPEntityFingerprint {
init?(response : NSURLResponse) {
if let httpResponse = response as? NSHTTPURLResponse {
let h = httpResponse.allHeaderFields
contentMD5 = h["Content-MD5"] as? String
etag = h["Etag"] as? String
lastModified = h["Last-Modified"] as? String
if contentMD5 == nil && etag == nil && lastModified == nil {
return nil
}
} else {
return nil
}
}
static func match(first : HTTPEntityFingerprint?, second : HTTPEntityFingerprint?) -> Bool {
if let a = first, b = second {
if let md5A = a.contentMD5, md5B = b.contentMD5 {
return md5A == md5B
}
if let etagA = a.etag, etagB = b.etag {
return etagA == etagB
}
if let lastA = a.lastModified, lastB = b.lastModified {
return lastA == lastB
}
}
return false
}
}
When I get an NSHTTPURLResponse from an NSURLSession, I create an HTTPEntityFingerprint from it and compare it against a previously stored fingerprint using HTTPEntityFingerprint.match. If the fingerprints match, then the HTTP resource hasn't changed and thus I do not need to deserialized the JSON response again; however, if the fingerprints do not match, then I deserialize the JSON response and save the new fingerprint.
This mechanism only works if your server returns at least one of the 3 entity headers: Content-MD5, Etag, or Last-Modified.
More Details on NSURLSession and NSURLCache Behavior
The caching provided by NSURLSession via NSURLCache is transparent, meaning when you request a previously cached resource NSURLSession will call the completion handlers/delegates as if a 200 response occurred.
If the cached response has expired then NSURLSession will send a new request to the origin server, but will include the If-Modified-Since and If-None-Match headers using the Last-Modified and Etag entity headers in the cached (though expired) result; this behavior is built in, you don't have to do anything besides enable caching. If the origin server returns a 304 (Not Modified), then NSURLSession will transform this to a 200 response the application (making it look like you fetched a new copy of the resource, even though it was still served from the cache).
This could be done with simple HTTP standard response.
Assume previous response is something like below:
{ status code: 200, headers {
"Accept-Ranges" = bytes;
Connection = "Keep-Alive";
"Content-Length" = 47616;
Date = "Thu, 23 Jul 2015 10:47:56 GMT";
"Keep-Alive" = "timeout=5, max=100";
"Last-Modified" = "Tue, 07 Jul 2015 11:28:46 GMT";
Server = Apache;
} }
Now use below to tell server not to send date if it is not modified since.
NSURLSession is a configurable container, you would probably need to use http option "IF-Modified-Since"
Use below configuration kind before downloading the resource,
NSURLSessionConfiguration *backgroundConfigurationObject = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:#"myBackgroundSessionIdentifier"];
[backgroundConfigurationObject setHTTPAdditionalHeaders:
#{#"If-Modified-Since": #"Tue, 07 Jul 2015 11:28:46 GMT"}];
if resource for example doesn't change from above set date then below delegate will be called
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) downloadTask.response;
if([httpResponse statusCode] == 304)
//resource is not modified since last download date
}
Check the downloadTask.response status code is 304 .. then resource is not modified and the resource is not downloaded.
Note save the previous success full download date in some NSUserDefaults to set it in if-modifed-since