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
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)
}
}
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)
I am working on get data from client server(ratin24 API). The API basically work after Authentication means I have one certificate and I was authenticate it with NSURLSession "didReceiveChallenge" delegate method. Everything is working fine but now issue is that I Got only header parts as a response not BOTY. so how to get actual data from there. I Pass XML Parameter in request body and the response should be XML but Got only header so please help me how to get BODY data in this situation.
let xmlString = "<?xml version='1.0' encoding='ISO-8859-1'?><TICKETANYWHERE><COUPON VER='1.0'><TEMPLATELIST /></COUPON></TICKETANYWHERE>"
let xmlData = xmlString.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
let request = NSMutableURLRequest(URL: NSURL(string: "My URL")!)
request.HTTPMethod = "POST"
request.HTTPBody = xmlData
request.addValue("text/html; charset=ISO-8859-1", forHTTPHeaderField: "Content-Type")
struct SessionProperties {
static let identifier : String! = "url_session_background_download"
}
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
let backgroundSession = NSURLSession(configuration: configuration, delegate:self as? NSURLSessionDelegate, delegateQueue: NSOperationQueue.mainQueue())
let downloadTask = backgroundSession.downloadTaskWithRequest(request){ (data, response, error) -> Void in
let httpResponse = response as! NSHTTPURLResponse
let statusCode = httpResponse.statusCode
if (statusCode == 200) {
print("Everyone is fine, file downloaded successfully.")
}
}
downloadTask.resume()
Response Data (only Header) body ? :
status code: 200, headers {
Connection = close;
"Content-Length" = 23113;
"Content-Type" = "text/html; charset=ISO-8859-1";
Date = "Mon, 27 Jun 2016 11:36:12 GMT";
Server = "Apache/2.2.15 (CentOS)";
}
The response object isn't supposed to contain the body data. An NSURLResponse object contains only metadata, such as the status code and headers. The actual body data should be in the NSData object for a data task, or in the provided file for a download task.
Note that for a download task the first parameter is an NSURL, not an NSData object. That NSURL contains the location of a file on disk from which you must immediately read the response data in your completion handler or move the file to a permanent location.
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
This question already has answers here:
Loading/Downloading image from URL on Swift
(39 answers)
Closed 7 years ago.
I am trying to downlaod an image.
This is the link of the image
https://www.google.it/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png
what I did is:
let url = NSURL(string: "https://www.google.it/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png")
let session = NSURLSession.sharedSession()
let task = session.downloadTaskWithURL(url!, completionHandler: {(url, response, error) in
if let error = error {
print("error = \(error)")
}
if let response = response {
print("response = \(response)")
}
})
task.resume()
really i have a response, and i can see it in the log as the following:
response = <NSHTTPURLResponse: 0x7fc1a1427060> { URL: https://www.google.it/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png } { status code: 200, headers {
"Cache-Control" = "private, max-age=31536000";
"Content-Length" = 13504;
"Content-Type" = "image/png";
Date = "Mon, 23 Nov 2015 23:57:55 GMT";
Expires = "Mon, 23 Nov 2015 23:57:55 GMT";
"Last-Modified" = "Fri, 04 Sep 2015 22:33:08 GMT";
Server = sffe;
"alt-svc" = "quic=\"www.google.com:443\"; p=\"1\"; ma=600,quic=\":443\"; p=\"1\"; ma=600";
"alternate-protocol" = "443:quic,p=1";
"x-content-type-options" = nosniff;
"x-xss-protection" = "1; mode=block";
} }
my problem is that there is no data in the response to get the actual image. I used to call the dataTaskWithRequest and the clouser for it have a data like this:
session.dataTaskWithRequest(request, completionHandler: {(data, response, error)
but here i didn't find the data, what is my wrong please?
When using a "download" task, you need to implement a session delegate method to get the data. Download tasks put the data into a temporary file, then call the delegate to tell it where that file is located.
If you implement URLSession:downloadTask:didFinishDownloadingToURL: in your download delegate, that method will be called with a file URL pointing to the temporary file.
If the images aren't too big, you can use data-style tasks instead. They download the data to memory, then give you an NSData object in the completion handler.