I can't consistently read response from a server if I use code below.
Header:
#import <Foundation/Foundation.h>
@interface TestHttpClient : NSObject<NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDownloadDelegate>
-(void)POST:(NSString*) relativePath payLoad:(NSData*)payLoad;
@end
Implementation:
#import "TestHttpClient.h"
@implementation TestHttpClient
-(void)POST:(NSString*)relativePath payLoad:(NSData*)payLoad
{
NSURL* url = [NSURL URLWithString:@"http://apps01.ditat.net/mobile/batch"];
// Set URL credentials and save to storage
NSURLCredential *credential = [NSURLCredential credentialWithUser:@"BadUser" password:@"BadPassword" persistence: NSURLCredentialPersistencePermanent];
NSURLProtectionSpace *protectionSpace = [[NSURLProtectionSpace alloc] initWithHost:[url host] port:443 protocol:[url scheme] realm:@"Ditat mobile services endpoint" authenticationMethod:NSURLAuthenticationMethodHTTPBasic];
[[NSURLCredentialStorage sharedCredentialStorage] setDefaultCredential:credential forProtectionSpace:protectionSpace];
// Configure session
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration ephemeralSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 30.0;
sessionConfig.timeoutIntervalForResource = 60.0;
sessionConfig.HTTPMaximumConnectionsPerHost = 1;
sessionConfig.URLCredentialStorage = [NSURLCredentialStorage sharedCredentialStorage]; // Should this line be here??
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:[NSOperationQueue mainQueue]];
// Create request object with parameters
NSMutableURLRequest *request =
[NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:60.0];
// Set header data
[request setHTTPMethod:@"POST"];
[request setValue:@"application/x-protobuf" forHTTPHeaderField:@"Content-Type"];
[request setValue:@"Version 1.0" forHTTPHeaderField:@"User-Agent"];
[request setValue:@"Demo" forHTTPHeaderField:@"AccountId"];
[request setValue:@"1234-5678" forHTTPHeaderField:@"DeviceSerialNumber"];
[request setValue:@"iOS 7.1" forHTTPHeaderField:@"OSVersion"];
[request setHTTPBody:payLoad];
// Call session to post data to server??
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request];
[downloadTask resume];
}
-(void)invokeDelegateWithResponse:(NSHTTPURLResponse *)response fileLocation:(NSURL*)location
{
NSLog(@"HttpClient.invokeDelegateWithResponse - code %ld", (long)[response statusCode]);
}
#pragma mark - NSURLSessionDownloadDelegate
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
NSLog(@"NSURLSessionDownloadDelegate.didFinishDownloadingToURL");
[self invokeDelegateWithResponse:(NSHTTPURLResponse*)[downloadTask response] fileLocation:location];
[session invalidateAndCancel];
}
// Implemented as blank to avoid compiler warning
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
}
// Implemented as blank to avoid compiler warning
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes
{
}
Can be called from any VC (place code under button action for example)
-(IBAction)buttonTouchUp:(UIButton *)sender
{
TestHttpClient *client = [[TestHttpClient alloc] init];
[client POST:@"" payLoad:nil];
return;
}
If you start program and call this code - it will show in NSLog completion with 401. Second try - will not work. Or might work if wait a little. But it won't send server requests as you push button.
NSURLSession somehow "remembers" failed attempts and won't return anything? Why is this behavior? I want to see 2 NSLog messages every time I push button.
TL;DR; You are not handling authentication correctly in your example.
This is what happens when an iOS or MacOS client encounters a URL that requires authentication:
The client requests a resource from the server
GET www.example.com/protected
The server response for that request has a status code of 401 and includes the WWW-Authenticate header. This tells the client this is a protected resource, and specifies what authentication method to use to access the resource. In iOS and MacOS, this is the "authentication challenge" that a delegate responds to. The WWW-Authenticate header is specifically mentioned in the documentation to highlight this.
Normally on iOS and MacOS, if a delegate is not provided or does not handle authentication challenges, the URL loading system will try to find an appropriate credential for this resource and authentication type by looking in NSURLCredentialStorage. It looks for a matching credential that has been saved as the default.
If a delegate implementing authentication IS provided, it's up to that delegate to provide a credential for that resource.
When the system has a credential for the authentication challenge the request is attempted again with the credential.
GET www.example.com/protected Authorization: Basic blablahaala
This explains the behavior you see in Charles, and is the correct behavior according to the various HTTP specifications.
Obviously, if you do not want to implement a delegate for your connection you have the option of putting a credential for the resource you are accessing in NSURLCredentialStorage. The system will use this, and will not require you to implement a delegate for the credential.
Create an NSURLCredential:
NSURLCredentialPersistencePermanent
will tellNSURLCredentialStorage
to store this permanently in the keychain. There are other possible values you can use, such asNSURLCredentialPersistenceForSession
. These are covered in the documentation. . You should avoid usingNSURLCredentialPersistencePermanent
with credentials that have not been validated, use session or none until the credential has been validated. You have probably seen projects using 'KeychainWrapper' or directly accessing the Keychain API to save internet usernames and passwords - this is not the preferred way to do this,NSURLCredentialStorage
is.Create an
NSURLProtectionSpace
with the correct host, port, protocol, realm, and authentication method:Note that
[[url port] integerValue]
will not provide defaults for HTTP (80) or HTTPS (443). You must provide those. The realm MUST match what the server provides.Finally, put it into
NSURLCredentialStorage
:This will allow the URL loading system to use this credential from this point forward. Essentially the same process can also be used for a SSL/TLS server trust reference as well.
In your question you are handling server trust, but not the
NSURLAuthenticationMethodHTTPBasic
. When your application receives an authentication challenge for the HTTP Basic Auth, you are not responding to it, and things go downhill from there. In your case, you probably do not need to implementURLSession:didReceiveChallenge:completionHandler:
at all if you do the steps above to set the default basic authentication credential for this protection space. The system will handle theNSURLAuthenticationMethodServerTrust
by performing the default trust evaluation. The system will then find the default credential you set for this protection space for basic authentication and use that.UPDATE
Based on new information in the comments, and running a modified version of the code, OP is actually getting this error in response to his request:
NSURLConnection/CFURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9813)
This error can be found in SecureTransport.h. The root certificate on the server credentials doesn't exist or is not trusted by the system. This is very rare, but can happen. Technote 2232 explains how to customize the server trust evaluation in the client to allow this certificate.