NSURLProtocol. requestIsCacheEquivalent never called - ios

I'm not sure what the deal is here, but the function:
class func requestIsCacheEquivalent(a: NSURLRequest, toRequest b: NSURLRequest) -> Bool
is never called within my NSURLProtocol subclass. I've even seen cases of the cache being used (verified by using a network proxy and seeing no calls being made) but this method just never gets invoked. I'm at a loss for why this is.
The problem I'm trying to solve is that I have requests that I'd like to cache data for, but these requests have a signature parameter that's different for each one (kind of like a nonce). This makes it so the cache keys are not the same despite the data being equivalent.
To go into explicit detail:
I fire a request with a custom signature (like this:
www.example.com?param1=1&param2=2&signature=1abcdefabc312093)
The request comes back with an Etag
The Etag is supposed to be managed by the NSURLCache but since it thinks that a different request (www.example.com?param1=1&param2=2&signature=1abdabcda3359809823) is being made it doesn't bother.
I thought that using NSURLProtocol would solve all my problems since Apple's docs say:
class func requestIsCacheEquivalent(_ aRequest: NSURLRequest,
toRequest bRequest: NSURLRequest) -> Bool
YES if aRequest and bRequest are equivalent for cache purposes, NO
otherwise. Requests are considered equivalent for cache purposes if
and only if they would be handled by the same protocol and that
protocol declares them equivalent after performing
implementation-specific checks.
Sadly, the function is never called. I don't know what the problem could be...
class WWURLProtocol : NSURLProtocol, NSURLSessionDataDelegate {
var dataTask: NSURLSessionDataTask?
var session: NSURLSession!
var trueRequest: NSURLRequest!
private lazy var netOpsQueue: NSOperationQueue! = NSOperationQueue()
private lazy var delegateOpsQueue: NSOperationQueue! = NSOperationQueue()
override class func canInitWithRequest(request: NSURLRequest) -> Bool {
println("can init with request called")
return true
}
override class func canonicalRequestForRequest(request: NSURLRequest) -> NSURLRequest {
println("canonical request for request called")
return request
}
override class func requestIsCacheEquivalent(a: NSURLRequest, toRequest b: NSURLRequest) -> Bool {
// never ever called?!?
let cacheKeyA = a.allHTTPHeaderFields?["CacheKey"] as? String
let cacheKeyB = b.allHTTPHeaderFields?["CacheKey"] as? String
println("request is cache equivalent? \(cacheKeyA) == \(cacheKeyB)")
return cacheKeyA == cacheKeyB
}
override func startLoading() {
println("start loading")
let sharedSession = NSURLSession.sharedSession()
let config = sharedSession.configuration
config.URLCache = NSURLCache.sharedURLCache()
self.session = NSURLSession(configuration: config, delegate: self, delegateQueue: self.delegateOpsQueue)
dataTask = session.dataTaskWithRequest(request, nil)
dataTask?.resume()
}
override func stopLoading() {
println("stop loading")
dataTask?.cancel()
}
//SessionDelegate
func URLSession(session: NSURLSession, didBecomeInvalidWithError error: NSError?) {
println("did become invalid with error")
client?.URLProtocol(self, didFailWithError: error!)
}
func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
println("did complete with error")
if error == nil {
client?.URLProtocolDidFinishLoading(self)
} else {
client?.URLProtocol(self, didFailWithError: error!)
}
}
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveResponse response: NSURLResponse, completionHandler: (NSURLSessionResponseDisposition) -> Void) {
println("did receive response")
client?.URLProtocol(self, didReceiveResponse: response, cacheStoragePolicy: .Allowed)
completionHandler(.Allow)
}
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
println("did receive data called")
client?.URLProtocol(self, didLoadData: data)
}
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, willCacheResponse proposedResponse: NSCachedURLResponse, completionHandler: (NSCachedURLResponse!) -> Void) {
println("will cache response called")
client?.URLProtocol(self, cachedResponseIsValid: proposedResponse)
completionHandler(proposedResponse)
}
I registered the protocol in my app delegate as follows:
NSURLProtocol.registerClass(WWURLProtocol.self)
I trigger the protocol as follows:
#IBAction func requestData(endpointString: String) {
let url = NSURL(string: endpointString)
let request = NSMutableURLRequest(URL: url!)
var cacheKey = endpointString
request.setValue("\(endpointString)", forHTTPHeaderField: "CacheKey")
request.cachePolicy = .UseProtocolCachePolicy
NSURLConnection.sendAsynchronousRequest(request, queue: netOpsQueue) { (response, data, error) -> Void in
if data != nil {
println("succeeded with data:\(NSString(data: data, encoding: NSUTF8StringEncoding)))")
}
}
}

I think that in practice, the loading system just uses the canonicalized URL for cache purposes, and does a straight string comparison. I'm not certain, though. Try appending your nonce when you canonicalize it, in some form that is easily removable/detectable (in case it is already there).

Your code seems all right.You just follow documents of Apple about URLProtocol.You could try to use URLSession for NSURLConnection is deprecated in newer iOS version.Good luck.

Related

How to mock URLSession.DataTaskPublisher

How can I mock URLSession.DataTaskPublisher? I have a class Proxy that require to inject a URLSessionProtocol
protocol URLSessionProtocol {
func loadData(from url: URL) -> URLSession.DataTaskPublisher
}
class Proxy {
private let urlSession: URLSessionProtocol
init(urlSession: URLSessionProtocol) {
self.urlSession = urlSession
}
func get(url: URL) -> AnyPublisher<Data, ProxyError> {
// Using urlSession.loadData(from: url)
}
}
This code was originally used with the traditional version of URLSession with the completion handler. It was perfect since I could easily mock URLSession for testing like Sundell's solution here: Mocking in Swift.
Is it possible to do the same with the Combine Framework?
In the same way that you can inject a URLSessionProtocol to mock a concrete session, you can also inject a mocked Publisher. For example:
let mockPublisher = Just(MockData()).eraseToAnyPublisher()
However, depending on what you do with this publisher you might have to address some weirdnesses with Combine async publishers, see this post for additional discussion:
Why does Combine's receive(on:) operator swallow errors?
The best way to test your client is to use URLProtocol.
https://developer.apple.com/documentation/foundation/urlprotocol
You can intercept all your request before she performs the real request on the cloud, and so creates your expectation. Once you have done your expectation, she will be destroyed, so you never make real requests.
Tests more reliable, faster, and you got the control!
You got a little example here: https://www.hackingwithswift.com/articles/153/how-to-test-ios-networking-code-the-easy-way
But it's more powerful than just this, you can do everything you want like: Check your Events/Analytics...
I hope it'll help you!
Since DataTaskPublisher uses the URLSession it is created from, you can just mock that. I ended up creating a URLSession subclass, overriding dataTask(...) to return a URLSessionDataTask subclass, which I fed with the data/response/error I needed...
class URLSessionDataTaskMock: URLSessionDataTask {
private let closure: () -> Void
init(closure: #escaping () -> Void) {
self.closure = closure
}
override func resume() {
closure()
}
}
class URLSessionMock: URLSession {
var data: Data?
var response: URLResponse?
var error: Error?
override func dataTask(with request: URLRequest, completionHandler: #escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
let data = self.data
let response = self.response
let error = self.error
return URLSessionDataTaskMock {
completionHandler(data, response, error)
}
}
}
Then obviously you just want your networking layer using this URLSession, I went with a factory to do this:
protocol DataTaskPublisherFactory {
func make(for request: URLRequest) -> URLSession.DataTaskPublisher
}
Then in your network layer:
func performRequest<ResponseType>(_ request: URLRequest) -> AnyPublisher<ResponseType, APIError> where ResponseType : Decodable {
Just(request)
.flatMap {
self.dataTaskPublisherFactory.make(for: $0)
.mapError { APIError.urlError($0)} } }
.eraseToAnyPublisher()
}
Now you can just pass a mock factory in the test using the URLSession subclass (this one asserts URLErrors are mapped to a custom error, but you could also assert some other condition given data/response):
func test_performRequest_URLSessionDataTaskThrowsError_throwsAPIError() {
let session = URLSessionMock()
session.error = TestError.test
let dataTaskPublisherFactory = mock(DataTaskPublisherFactory.self)
given(dataTaskPublisherFactory.make(for: any())) ~> {
session.dataTaskPublisher(for: $0)
}
let api = API(dataTaskPublisherFactory: dataTaskPublisherFactory)
let publisher: AnyPublisher<TestCodable, APIError> =
api.performRequest(URLRequest(url: URL(string: "www.someURL.com")!))
let _ = publisher.sink(receiveCompletion: {
switch $0 {
case .failure(let error):
XCTAssertEqual(error, APIError.urlError(URLError(_nsError: NSError(domain: "NSURLErrorDomain", code: -1, userInfo: nil))))
case .finished:
XCTFail()
}
}) { _ in }
}
The one issue with this is that URLSession init() is deprecated from iOS 13, so you have to live with a warning in your test. If anyone can see a way around that I'd greatly appreciate it.
(Note: I'm using Mockingbird for mocks).

URLSession in swift 4 without completion handler

can anyone tell me what all URLSession delegate methods should me implemented along with an example in swift 4.
I DO NOT WANT TO USE COMPLETION HANDLER.
My app is in objective c, and i am converting it in swift.
earlier it uses NSURLConnection which is now deprecated.
So it's little confusing for me to use URLSession.
Thanks!
EDIT:
I am using something like
let defaultSession = URLSession(configuration: defaultSessionConfiguration, delegate: self, delegateQueue: nil)
let sessionTask = defaultSession.dataTask(with: requests)
sessionTask.resume()
Now i want to implement methods which will handle senarios when it receives response, data or error.
I am looking for equivalent of
func connection(_ connection: NSURLConnection, didReceive data: Data){}
func connection(_ connection: NSURLConnection, didFailWithError error: Error){}
func connection(_ connection: NSURLConnection, canAuthenticateAgainstProtectionSpace protectionSpace: URLProtectionSpace) -> Bool {
return protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust
}
func connection(_ connection: NSURLConnection, didReceive challenge: URLAuthenticationChallenge) {
if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust) {
if let aTrust = challenge.protectionSpace.serverTrust {
challenge.sender?.use(URLCredential(trust: aTrust), for: challenge)
}
}
challenge.sender?.continueWithoutCredential(for: challenge)
}

WebView can't handle response when using a custom NSURLProtocol

I'm using a custom NSURLProtocol in order to be able to save the username and password from a form that is loaded in a webView and automatically log the user in.
My issue is that I can get the data, but I can't complete the action to login the user as the webpage uses some JS to listen to the response and complete the form action.
Is there a way to do it?
Here's my current CustomURLProtocol class:
class CustomURLProtocol: NSURLProtocol {
var connection: NSURLConnection!
override class func canInitWithRequest(request: NSURLRequest) -> Bool {
if NSURLProtocol.propertyForKey("customProtocolKey", inRequest: request) != nil {
return false
}
return true
}
override class func canonicalRequestForRequest(request: NSURLRequest) -> NSURLRequest {
return request
}
override func startLoading() {
let newRequest = request.mutableCopy() as! NSMutableURLRequest
NSURLProtocol.setProperty(true, forKey: "customProtocolKey", inRequest: newRequest)
// Look for user credentials
if request.URL?.description == "https://www.website.com/Account/Login" {
if let data = request.HTTPBody {
let dataString: String = NSString(data: data, encoding: NSASCIIStringEncoding) as! String
// Code for reading user credentials goes here
}
}
connection = NSURLConnection(request: newRequest, delegate: self)
}
override func stopLoading() {
if connection != nil {
connection.cancel()
}
connection = nil
}
func connection(connection: NSURLConnection!, didReceiveResponse response: NSURLResponse!) {
client!.URLProtocol(self, didReceiveResponse: response, cacheStoragePolicy: .NotAllowed)
}
func connection(connection: NSURLConnection!, didReceiveData data: NSData!) {
client!.URLProtocol(self, didLoadData: data)
}
func connectionDidFinishLoading(connection: NSURLConnection!) {
client!.URLProtocolDidFinishLoading(self)
}
func connection(connection: NSURLConnection!, didFailWithError error: NSError!) {
client!.URLProtocol(self, didFailWithError: error)
}
}
Is there something I'm missing or is wrong in my logic?
Thanks in advance
So if I understand correctly, you're intercepting the call just to preserve the credentials so you can log the user in automatically later? If you only care about the login request you could move the url check to canInitWithRequest, but that's a side issue.
Using a custom NSURLProtocol correctly shouldn't affect the response, so the web page should behave normally. I notice that you're using NSURLConnection, which is deprecated, so you might want to look at using NSURLSession but again that's a side issue.
Have you stepped through the code to see where it's failing? Are your connection delegate methods being called? Is "webViewDidFinishLoad" being called on the web view?
Or are you not talking about the initial form submission, but subsequent ones when you're trying to log in the user with the stored credentials?

Default implementation of protocol method with Swift extension

I'm trying to write default behaviour for a delegate method using a Swift extension as below, but it is never called. Does anyone know why or how to do it the right way?
extension NSURLSessionDelegate {
public func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {
//default behaviour here
}
}
Adding override does not work either.
According to this,
Apple's default implementation looks like:
extension NSURLSessionDelegate {
func URLSession(session: NSURLSession, didBecomeInvalidWithError error: NSError?) { }
func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) { }
}
My DataTask calls typically look like this:
let sessionConfiguration = NSURLSessionConfiguration.defaultSessionConfiguration()
sessionConfiguration.HTTPCookieStorage = NSHTTPCookieStorage.sharedHTTPCookieStorage()
let session = NSURLSession(configuration: sessionConfiguration)
let requestURL = NSURL(string:"https:www.google.com/blabla")
session.dataTaskWithURL(requestURL!, completionHandler: completion).resume()
Where completion will typically be a Swift closure received via parameter.
I need to implement the URLSession(... didReceiveChallenge ...) function for all nsurlsessiontask implementations throughout my app, but can't set my session's delegate as I need to use the completionHandler (as mentioned in my comment below).
You can extends the NSURLSessionDelegate protocol for adding default implementation, but your NSURLSession objects needs a delegate.
This delegate can only be set using +sessionWithConfiguration:delegate:delegateQueue: (since the delegate property is read only), so your only way to set it is to subclass NSURLSession, override +sessionWithConfiguration: and call the initializer with the delegate property. The issue here is that you have to replace all your NSURLSession objects to MyCustomSessionClass. objects.
I suggest you to create a SessionCreator class which will conforms to NSURLSessionDelegate protocol and will create NSURLSessionobjects. You still have to replace the creation of your objects, but at least the object isn't the delegate of itself.
public class SessionCreator:NSObject,NSURLSessionDelegate {
//MARK: - Singleton method
class var sharedInstance :SessionCreator {
struct Singleton {
static let instance = SessionCreator()
}
return Singleton.instance
}
//MARK: - Public class method
public class func createSessionWithConfiguration (configuration:NSURLSessionConfiguration) -> NSURLSession {
return sharedInstance.createSessionWithConfiguration(configuration)
}
//MARK: - Private methods
private func createSessionWithConfiguration (configuration:NSURLSessionConfiguration) -> NSURLSession {
return NSURLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}
//MARK: - NSURLSessionDelegate protocol conformance
public func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {
// Always called since it's the delegate of all NSURLSession created using createSessionWithConfiguration
}
}
// Let create a NSURLSession object :
let session = SessionCreator.createSessionWithConfiguration(NSURLSessionConfiguration())

How Alamofire could guarantee response method would be called after get all the response?

Recently I read the source code of Alamofire, and I am really confused about How could Alamofire guarantee the response method would be called in correctly order. I hope someone(maybe matt lol) could help me out.
Example, an easy GET Request like this
Alamofire.request(.GET, "https://api.github.com/users/octocat/received_events")
After I analysed the work flow of it, I posted my understanding
Create the request and underlying NSURLSession.
public func request(method: Method, URLString: URLStringConvertible, parameters: [String: AnyObject]? = nil, encoding: ParameterEncoding = .URL) -> Request {
return Manager.sharedInstance.request(method, URLString, parameters: parameters, encoding: encoding)
}
This method would create a request: Request object, which would contain the underlying NSURLSessionDataTask object. Manager.sharedInstance has already set up a NSURLSession and set itself as that session's delegate. The Manager.sharedInstance object would save a customized object request.delegate in its own property delegate
After those objects were created, Alamofire would send this request immediately.
public func request(URLRequest: URLRequestConvertible) -> Request {
var dataTask: NSURLSessionDataTask?
dispatch_sync(queue) {
dataTask = self.session.dataTaskWithRequest(URLRequest.URLRequest)
}
let request = Request(session: session, task: dataTask!)
delegate[request.delegate.task] = request.delegate
if startRequestsImmediately {
request.resume()
}
return request
}
Since Manager.sharedInstance set itself as the underlying NSURLSession's delegate, when data received, the delegate methods would be called
public func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
if dataTaskDidReceiveData != nil {
dataTaskDidReceiveData!(session, dataTask, data)
} else if let delegate = self[dataTask] as? Request.DataTaskDelegate {
delegate.URLSession(session, dataTask: dataTask, didReceiveData: data)
}
}
If a user want to get the response and do something related, he would use following public API
// Here the request is a Request object
self.request?.responseString { (request, response, body, error) in
// Something do with the response
}
let's see what the responseString(_: completionHandler:) method does
public func response(queue: dispatch_queue_t? = nil, serializer: Serializer, completionHandler: (NSURLRequest, NSHTTPURLResponse?, AnyObject?, NSError?) -> Void) -> Self {
delegate.queue.addOperationWithBlock {
let (responseObject: AnyObject?, serializationError: NSError?) = serializer(self.request, self.response, self.delegate.data)
dispatch_async(queue ?? dispatch_get_main_queue()) {
completionHandler(self.request, self.response, responseObject, self.delegate.error ?? serializationError)
}
}
return self
}
My question is how 5 could be guaranteed to happen after 3, so the user could get all the response not part of it, because self.response at this time would be fully loaded.
Is it because of NSURLSession's background processing occurs on the same queue -- delegate.queue, which is created like this in Alamofire:
//class Request.TaskDelegate: NSObject, NSURLSessionTaskDelegate
self.queue = {
let operationQueue = NSOperationQueue()
operationQueue.maxConcurrentOperationCount = 1
operationQueue.suspended = true
if operationQueue.respondsToSelector("qualityOfService") {
operationQueue.qualityOfService = NSQualityOfService.Utility
}
return operationQueue
}()
Is my understanding correct, how does that happen? It might be require some understanding about RunLoop and NSURLSession's thread mechanism, if you could point out where I could refer to, thank you as well.

Resources