I am implementing a custom NSURLProtocol, and internally want to use NSURLSession with data tasks for internal networking instead of NSURLConnection.
I have hit an interesting problem and wonder about the internal implementation of the challenge handler of NSURLSession/NSURLSessionTask.
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler;
Here I am basically provided with two different challenge handlers, one being the completionHandler block, which is provided with all necessary information to handle the challenge, but there is also the legacy NSURLAuthenticationChallenge.client which has methods that correspond pretty much one to one with the completionHandler information options.
Since I am developing a protocol, and would like to pass certain authentication challenges upward the URL loading system for the calling API to implement, I need to use the NSURLSession client method:
- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
My question is whether the internal implementation of the completionHandler and NSURLAuthenticationChallenge.client is the same, and if so, can I skip calling the completion handler in the delegate method, with expectance that the URL loading system will call the appropriate NSURLAuthenticationChallenge.client method?
To answer my own question, the answer is no. Moreover, Apple's provided challenge sender does not implement the entire NSURLAuthenticationChallengeSender protocol, thus crashing when client attempts to respond to challenge:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFURLSessionConnection performDefaultHandlingForAuthenticationChallenge:]: unrecognized selector sent to instance 0x7ff06d958410'
My solution was to create a wrapper:
#interface CPURLSessionChallengeSender : NSObject <NSURLAuthenticationChallengeSender>
- (instancetype)initWithSessionCompletionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler;
#end
#implementation CPURLSessionChallengeSender
{
void (^_sessionCompletionHandler)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential);
}
- (instancetype)initWithSessionCompletionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
self = [super init];
if(self)
{
_sessionCompletionHandler = [completionHandler copy];
}
return self;
}
- (void)useCredential:(NSURLCredential *)credential forAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
_sessionCompletionHandler(NSURLSessionAuthChallengeUseCredential, credential);
}
- (void)continueWithoutCredentialForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
_sessionCompletionHandler(NSURLSessionAuthChallengeUseCredential, nil);
}
- (void)cancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
{
_sessionCompletionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
}
- (void)performDefaultHandlingForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
_sessionCompletionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}
- (void)rejectProtectionSpaceAndContinueWithChallenge:(NSURLAuthenticationChallenge *)challenge
{
_sessionCompletionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil);
}
#end
And I replace the challenge object with a new one, using my wrapped sender:
NSURLAuthenticationChallenge* challengeWrapper = [[NSURLAuthenticationChallenge alloc] initWithAuthenticationChallenge:challenge sender:[[CPURLSessionChallengeSender alloc] initWithSessionCompletionHandler:completionHandler]];
[self.client URLProtocol:self didReceiveAuthenticationChallenge:challengeWrapper];
Related
The execution of my app usually stops for 2-3 seconds (even 5 seconds) at didReceiveChallenge. Around 1 out of 10 times it takes forever.
The whole thing works, but what can I do to speed it up?
Here's my code:
- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * __nullable credential))completionHandler{
NSLog(#"*** KBRequest.NSURLSessionDelegate - didReceiveChallenge IOS10");
if([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]){
if([challenge.protectionSpace.host isEqualToString:#"engine.my.server"]){
NSURLCredential *credential = [NSURLCredential credentialForTrust: challenge.protectionSpace.serverTrust];
completionHandler(NSURLSessionAuthChallengeUseCredential,credential);
}
else{
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
}
}
}
You MUST call the completion handler EVERY time this method is called. You're only calling it for a single protection space.
When the OS calls this method on your delegate, the NSURLSession stack sits there dutifully waiting for you to call the completion handler block. If you fail to call the completion handler, your request will just sit in limbo until the request times out.
To fix this, at the bottom of the method, add:
} else {
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}
I have a class named RestService which I use all over my app to perform several synchronous requests to a web service. I added a new method to this class to perform an asynchronous request which again I want to reuse all over my app. This is the code for that new method:
- (void)backgroundExecutionOfService:(NSString *)serviceName
withParameters:(NSDictionary *)parameters
inView:(UIView *)view
withDelegate:(UIViewController *)delegate
{
NSString *serviceUrl = #"http://MyWebServer/public/api/clients/5";
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.allowsCellularAccess = YES;
sessionConfig.timeoutIntervalForRequest = 10;
sessionConfig.timeoutIntervalForResource = 10;
sessionConfig.HTTPMaximumConnectionsPerHost = 1;
NSURLSession *session;
session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:delegate
delegateQueue:nil];
NSURLSessionDownloadTask *getFileTask;
getFileTask = [session downloadTaskWithURL:[NSURL URLWithString:serviceUrl]];
[getFileTask resume];
}
But XCode is giving me a warning about using that parameter as a delegate (Sending UIViewController * __strong' to parameter of incompatible type 'id< NSURLSessionDelegate > _Nullable'). I made sure that the view controller I'm sending as a parameter has declared < NSURLSessionDelegate > in .h and I created the delegate methods in the ViewControllers's implementation file.
- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(nullable NSError *)error{
NSLog(#"Became invalid with error: %#", [error localizedDescription]);
}
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * __nullable credential))completionHandler{
NSLog(#"Received challenge");
}
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session{
NSLog(#"Did finish events for session: %#", session);
}
The app doesn't crash but the delegate methods are never called. The synchronous web services work as expected.
That happened because a UIViewController class don't conform a NSURLSessionDelegate protocol.
To resolve that discrepancy just change the method's signature to this like:
- (void)backgroundExecutionOfService:(NSString *)serviceName withParameters:(NSDictionary *)parameters inView:(UIView *)view withDelegate:(id<NSURLSessionDelegate>)delegate{
//... your code
}
And "read up on the basics of delegates."
I'm new to block programming with Objective-C, and I searched to try to find an answer to this, so apologies if this is a silly question.
I'm writing a class to wrap Imgur downloads. It's a subclass of NSObject and uses NSURLSession. I'm using a singleton pattern to allow me to easily fire off a download in one line, and get the progress of the image download and the UIImage itself once completed.
- (void)downloadImageWithURL:(NSURL *)URL
progressBlock:(void (^)(CGFloat percentageDownlaoded))progressBlock
completionHandler:(void (^)(UIImage *downloadedImage, NSURLResponse *response, NSError *error))completionHandler;
But I'm confused how I implement this in the class itself. Here's my full class file:
+ (instancetype)sharedClient {
static ImgurClient *sharedClient = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedClient = [[ImgurClient alloc] init];
});
return sharedClient;
}
- (id)init {
self = [super init];
if (self) {
self.session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:nil];
}
return self;
}
#pragma mark - External Download Methods
/**
* Asynchronously downloads the image for the given URL.
*/
- (void)downloadImageWithURL:(NSURL *)URL
progressBlock:(void (^)(CGFloat percentageDownlaoded))progressBlock
completionHandler:(void (^)(UIImage *downloadedImage, NSURLResponse *response, NSError *error))completionHandler {
}
/**
* Asynchronously downloads the thumbnail for the given URL at the specified size.
*/
- (void)downloadThumbnailWithID:(NSURL *)URL
size:(CSImgurThumbnailSize)size
progressBlock:(void (^)(CGFloat percentageDownlaoded))progressBlock
completionHandler:(void (^)(UIImage *downloadedImage, NSURLResponse *response, NSError *error))completionHandler {
}
#pragma mark - NSURLSessionDelegate Methods
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes {
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
}
How do I go about structuring this class? Do I have properties representing the progress then throw that into the appropriate methods?
If anyone could shed any light or give a link to an explanation I'd greatly appreciate it.
Here's parts of it in sketch form, I'll concentrate on one block to give you an idea, you'll have to fill in all the other parts yourself otherwise this answer will be huge.
You'll need to add properties to sharedClient to hold the blocks.
It will be much easier if you typedef the types of the blocks first i.e.
typedef void (^PercentageDownloadedBlock)(CGFloat percentageDownlaoded);
Then you can declare your property as
#property (copy, nonatomic) PercentageDownloadedBlock thePercentageBlock;
Then do something like this:
- (void)downloadThumbnailWithID:(NSURL *)URL
size:(CSImgurThumbnailSize)size
progressBlock:(PercentageDownloadedBlock)progressBlock
completionHandler:(completionHandlerBlock)completionHandler
{
self.thePercentageBlock = progressBlock;
start the connection
...
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
CFFloat progress = do calculation of progress
self.thePercentageBlock(progress);
...
}
etc.
I already done that and wrap it in a subclass of NSURLConnection. Hope this help. Check it here:-
https://github.com/hackiftekhar/IQURLConnection
Following blocks have been implemented:-
1) ResponseBlock //NSURLResponse
2) ProgressBlock //Value range from 0.0 to 1.0
3) CompletionBlock //NSData, NSError
Take a look at AFNetworking's AFURLSessionManager and AFHTTPSessionManager. It wraps the NSURLSession APIs in a way similar to what you describe. If you're using CocoaPods it's possible to use AFNetworking's AFURLSession APIs without necessarily importing the rest of the library. Otherwise taking a look at the code is a good way to see how to do what you want.
I am trying to make a Asynchronous Call , a Synchronous one. I know its not a better idea to do it. But, I do need such to code to handle Auth Challenge of Self Signed Certificate while Keeping the call still as Synchronous.
But, I am not sure whether it is a perfect way to make Asycnh call a Synch one.
-(NSData*) startConnection{
NSURLConnection *conn=[[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES];
while(!isFinished && [[NSRunLoop currentLoop] runMode: NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]){
}
return responseAppData;
}
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
//Code to handle Certificate
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
[responseAppData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
isFinished=YES;
}
I also thought of using the while Loop as below, so which one should be used?
while(!isFinished ){
}
Actually it's the opposite. If you want to handle these NSURLConnectionDelegate methods, you need to use asynchronous calls, NOT synchronous. Otherwise the delegates are never called.
typedef void (^onDownload)(NSData *data);
#property (nonatomic,assign) onDownload block;
-(void) startConnectionwithBlock:(onDownload) pBlock;{
self.block = [pBlock copy];
}
-(void) connectionDidFinishLoading:(NSURLConnection *)connection{
block(self.data);
}
I'm running a LOT of asynchronous (delegate, not block) NSURLConnections simultaneously, and they all come back very quickly as I'm hitting a LAN server.
Every so often, one NSURLConnection will go defunct and never return.
connection:willSendRequest: is called but connection:didReceiveResponse: (and failure) is not.
Any ideas? I'm wondering if I should make a simple drop-in replacement using CFNetwork instead.
Edit: There's really not much code to show. What I've done is created a wrapper class to download files. I will note that the problem happens less when I run the connection on a separate queue - but still happens.
The general gist of what I'm doing is creating a download request for each cell as a tableview scrolls (in cellForRowAtIndexPath) and then asynchronously loading in an image file to the table cell if the cell is still visible.
_request = [NSMutableURLRequest requestWithURL:_URL];
_request.cachePolicy = NSURLRequestReloadIgnoringCacheData;
_request.timeoutInterval = _timeoutInterval;
if(_lastModifiedDate) {
[_request setValue:[_lastModifiedDate RFC1123String] forHTTPHeaderField:#"If-Modified-Since"];
}
_connection = [[NSURLConnection alloc] initWithRequest:_request
delegate:self
startImmediately:NO];
[_connection start];
As requested, instance variables:
NSMutableURLRequest *_request;
NSURLConnection *_connection;
And delegate methods:
- (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response {
NSLog(#"%# send", _URL);
return request;
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
NSLog(#"%# response", _URL);
_response = (id)response;
// create output stream
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
_receivedLength += data.length;
_estimatedProgress = (Float32)_receivedLength / (Float32)_response.expectedContentLength;
[_outputStream write:data.bytes maxLength:data.length];
// notify delegate
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
// close output stream
// notify delegate
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(#"%# failure", _URL);
// notify delegate
}
- (void)connection:(NSURLConnection *)connection
didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
if(_credential && challenge.previousFailureCount == 0) {
[[challenge sender] useCredential:_credential forAuthenticationChallenge:challenge];
}
}
After poking around in profiler, I found a lead, and it gave me a hunch.
My credentials were failing (not sure why...) and so previousFailureCount was not 0, and hence I wasn't using my credential object.
Changed the code to this and I have no problems:
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
if(_credential) {
[[challenge sender] useCredential:_credential forAuthenticationChallenge:challenge];
}
}
A NSURLConnection will send either didReceiveResponse or didFailWithError.
Often, you're dealing with timeouts before didFailWithError occurs.