Test that NSURLSessionConfiguration settings are obeyed - ios

I created an NSURLSessionConfiguration with some default settings but when I see the request object made with that configuration in my custom NSURLProtocol it doesn't seem that all those settings are inherited and I'm a bit confused.
NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSMutableArray *protocolsArray = [NSMutableArray arrayWithArray:config.protocolClasses];
[protocolsArray insertObject:[CustomProtocol class] atIndex:0];
config.protocolClasses = protocolsArray;
// ex. set some random parameters
[config setHTTPAdditionalHeaders:#{#"Authorization":#"1234"}];
[config setAllowsCellularAccess:NO];
[config setRequestCachePolicy:NSURLRequestReturnCacheDataElseLoad];
[config setHTTPShouldSetCookies:NO];
[config setNetworkServiceType:NSURLNetworkServiceTypeVoice];
[config setTimeoutIntervalForRequest:4321];
// Create a request with this configuration and start a task
NSURLSession* session = [NSURLSession sessionWithConfiguration:config];
NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"https://google.com"]];
NSURLSessionDataTask* task = [session dataTaskWithRequest:request];
[task resume];
In my custom NSURLProtocol that is registered
- (void)startLoading {
...
// po [self.request valueForHTTPHeaderField:#"Authorization"] returns 1234
//
// However, I'm very confused why
//
// - allowsCellularAccess
// - cachePolicy
// - HTTPShouldHandleCookies
// - networkServiceType
// - timeoutInterval
//
// for the request return the default values unlike for the header
...
}
Is there some way to check that those parameters I've set are obeyed and inherited by the request?

When dealing with http requests, it is helpful to start with the basics, such as is the request actually being made by the OS, and is a response being received? This will in part help to answer your question about checking that set parameters are infact being obeyed by the request.
I would challenge your use of the word "inherit" in the phrase
Is there some way to check that those parameters I've set are obeyed and inherited by the request?
Inheritance in Object Oriented programming has a very specific meaning. Did you in fact create a custom subclass (let's call it SubClassA) of NSURLRequest with specific properties, and then a further subclass (let's call it SubClassB), and are expecting the second subclass (SubClassB) to inherit properties from its parent (SubClassA)? If so, this is certainly not indicated in the code you provided.
There are several HTTP Proxy programs available which help confirm whether or not the HTTP request is being sent, if a response is received, and also which allow you to inspect the details of the request and the response. Charles HTTP Proxy is one such program. Using Charles, I was able to determine that your code as provided is not making any HTTP request. So you cannot confirm or deny any parameters if the request is not being made.
By commenting out the lines including the CustomProtocol as part of the NSURLSession configuration, and running your code either with or without these lines, I gained some potentially valuable information:
by commenting out the lines including the CustomProtocol, a request was in fact made (and failed), as informed by Charles HTTP Proxy. I also added a completion block to your method dataTaskWithRequest. This completion block is hit when the CustomProtocol configuration lines are commented out. The CustomProtocol's startLoading method is not hit.
when leaving in the original lines to configure the NSURLSession using the CustomProtocol, there was no request recorded by Charles HTTP Proxy, and the completion handler is not hit. However, the CustomProtocol's startLoading method is hit.
Please see code below (modifications made to the code posted in the original question).
NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSMutableArray *protocolsArray = [NSMutableArray arrayWithArray:config.protocolClasses];
//[protocolsArray insertObject:[CustomProtocol class] atIndex:0];
//config.protocolClasses = protocolsArray;
// ex. set some random parameters
[config setHTTPAdditionalHeaders:#{#"Authorization":#"1234"}];
[config setAllowsCellularAccess:NO];
[config setRequestCachePolicy:NSURLRequestReturnCacheDataElseLoad];
[config setHTTPShouldSetCookies:NO];
[config setNetworkServiceType:NSURLNetworkServiceTypeVoice];
[config setTimeoutIntervalForRequest:4321];
// Create a request with this configuration and start a task
NSURLSession* session = [NSURLSession sessionWithConfiguration:config];
NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"https://google.com"]];
NSURLSessionDataTask* task = [session dataTaskWithRequest:request
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSString * auth = [request valueForHTTPHeaderField:#"Authorization"];
NSLog(#"Authorization: %#", auth);
BOOL allowsCellular = [request allowsCellularAccess];
NSString * allowsCellularString = allowsCellular ? #"YES" : #"NO";
NSLog(#"Allows cellular: %#", allowsCellularString);
}];
[task resume];
This gives you the information that the CustomProtocol is not properly handling the request. Yes, the breakpoint inside the startLoading method is hit when the CustomProtocol is configured as part of the NSURLSession, but that is not definitive proof that the CustomProtocol is handling the request properly. There are many steps necessary to using a CustomProtocol, as outlined by Apple (Protocol Support, NSURLProtocol Class Reference) that you should confirm you are following.
Some things to make sure are working:
if you are using a CustomProtocol, that means you are likely trying to handle a different protocol other than http, https, ftp, ftps, etc.
make sure that your end point (the server which is listening for the http requests and responding) can actually accept the request and reply.
if you are setting an HTTP Authorization Header, make sure that the server can respond appropriately, and that the credentials are valid if you are expecting a positive response
remember to register your CustomProtocol
for example:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
[NSURLProtocol registerClass:[CustomProtocol class]];
return YES;
}
Below is a unit tests to verify that the NSURLSession is functioning as expected (without using our custom protocol explicitly). Note that this unit test does pass when added to Apple's own sample code for the project CustomHTTPProtocol, but does not pass using our very bare bones CustomProtocol
- (void)testNSURLSession {
XCTestExpectation *expectation = [self expectationWithDescription:#"Testing standard NSURL Session"];
[[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:#"https://www.apple.com/"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
#pragma unused(data)
XCTAssertNil(error, #"NSURLSession test failed with error: %#", error);
if (error == nil) {
NSLog(#"success:%zd / %#", (ssize_t) [(NSHTTPURLResponse *) response statusCode], [response URL]);
[expectation fulfill];
}
}] resume];
[self waitForExpectationsWithTimeout:3.0 handler:^(NSError * _Nullable error) {
if(nil != error) {
XCTFail(#"NSURLSession test failed with error: %#", error);
}
}];
}
Below is a unit test which may be used to verify that the configurations made to a NSURLSession are as expected, when configuring using our own CustomProtocol class. Again, please note that this test fails using the empty implementation of CustomProtocol but this is expected if using Test Driven Development (create the test first, and then the code second which will allow the test to pass).
- (void)testCustomProtocol {
XCTestExpectation *expectation = [self expectationWithDescription:#"Testing Custom Protocol"];
NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSMutableArray *protocolsArray = [NSMutableArray arrayWithArray:config.protocolClasses];
[protocolsArray insertObject:[CustomProtocol class] atIndex:0];
config.protocolClasses = protocolsArray;
// ex. set some random parameters
[config setHTTPAdditionalHeaders:#{#"Authorization":#"1234"}];
[config setAllowsCellularAccess:NO];
[config setRequestCachePolicy:NSURLRequestReturnCacheDataElseLoad];
[config setHTTPShouldSetCookies:NO];
[config setNetworkServiceType:NSURLNetworkServiceTypeVoice];
[config setTimeoutIntervalForRequest:4321];
// Create a request with this configuration and start a task
NSURLSession* session = [NSURLSession sessionWithConfiguration:config];
NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"https://www.apple.com"]];
NSURLSessionDataTask* task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
#pragma unused(data)
XCTAssertNil(error, #"test failed: %#", error.description);
if (error == nil) {
NSLog(#"success:%zd / %#", (ssize_t) [(NSHTTPURLResponse *) response statusCode], [response URL]);
NSString * auth = [request valueForHTTPHeaderField:#"Authorization"];
NSLog(#"Authorization: %#", auth);
XCTAssertNotNil(auth);
BOOL allowsCellular = [request allowsCellularAccess];
XCTAssertTrue(allowsCellular);
XCTAssertEqual([request cachePolicy], NSURLRequestReturnCacheDataElseLoad);
BOOL shouldSetCookies = [request HTTPShouldHandleCookies];
XCTAssertTrue(shouldSetCookies);
XCTAssertEqual([request networkServiceType], NSURLNetworkServiceTypeVoice);
NSTimeInterval timeOutInterval = [request timeoutInterval];
XCTAssertEqualWithAccuracy(timeOutInterval, 4321, 0.01);
[expectation fulfill];
}
}];
[task resume];
[self waitForExpectationsWithTimeout:3.0 handler:^(NSError * _Nullable error) {
if(nil != error) {
XCTFail(#"Custom Protocol test failed with error: %#", error);
}
}];
}

Related

Make http DELETE request with NSURLSession

I'm trying to make an http DELETE request using NSURLSession, but it's not completely working. The server deletes the resource, but the NSURLSession method dataTaskWithRequest: completionHandler: returns a time out error after waiting for the specified timeout.
I am not using NSURLConnection because it is deprecated.
Of the NSURLSession methods to use, I chose dataTaskWithRequest because it is most similar to the method I use for http GET: dataTaskWithUrl: completionHandler. The methods beginning with "uploadTask" and "downloadTask" don't seem appropriate for a DELETE, but downloadTaskWithRequest: completionHandler: 'worked' in the same way as the dataTask method above. The server deleted the resource, but the method returned a time out error.
Here is the code:
+(void)httpDelete: (NSString*)url completionHandler: (void(^)(id, NSError*))complete
{
NSURLSessionConfiguration *urlSessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSMutableDictionary* dictionaryAdditionalHeaders = [[NSMutableDictionary alloc] init];
NSString* stringBearerToken = #"...";
NSString* stringApiKey = #"...";
[dictionaryAdditionalHeaders setObject:stringBearerToken forKey:#"Authorization"];
[dictionaryAdditionalHeaders setObject:stringApiKey forKey:#"x-api-key"];
[dictionaryAdditionalHeaders setObject:#"application/json" forKey:#"Content-Type"];
[dictionaryAdditionalHeaders setObject:#0 forKey:#"Content-Length"];
[urlSessionConfiguration setHTTPAdditionalHeaders: dictionaryAdditionalHeaders];
NSURLSession *urlSession = [NSURLSession sessionWithConfiguration: urlSessionConfiguration delegate:nil delegateQueue:[NSOperationQueue mainQueue]];
NSMutableURLRequest* mutableUrlRequest = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:url] cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:5];
[mutableUrlRequest setHTTPMethod: #"DELETE"];
[[urlSession dataTaskWithRequest:mutableUrlRequest completionHandler: ^(NSData *data, NSURLResponse* response, NSError* error)
{
if(error != nil)
{
complete(response, error);
}
else
{
complete(response, nil);
}
}] resume];
}
Using Postman, the DELETE call returns with a 204 immediately.
Am I using NSURLSession correctly for a delete request?
It turns out the Amazon API Gateway incorrectly sends a Content-Length header with a 204 response. They added the issue to their backlog March 21, 2016 according to this AWS forum. When I increased the timeout interval of the NSMutableURLRequest to a ridiculous 300 seconds, the dataTaskWithRequest method returns with a real response instead of timing out.
This isn't an error with NSURLSession - it means that your request is actually timing out. That means that there's an error on the back-end (maybe it's not reaching your server at all?)
Also, I've found these issues much easier to debug using a third-party framework to send my HTTP requests. AFNetworking is a really good one.

Perform some task while NSURLConnection loading data objective-c

I am a beginer in iOS programming. I have some problem with NSURLConnection: I have installed SWRevealViewController https://github.com/John-Lluch/SWRevealViewController and when my app is loading Data from server, I can't use interaction with screen. I can't open my SWR-menu while Data is loading.
Here is my SWR in viewDidLoad:
SWRevealViewController *revealViewController = self.revealViewController;
if ( revealViewController ) {
[self.openMenyItmet setTarget: self.revealViewController];
[self.openMenyItmet setAction: #selector( revealToggle: )];
[self.view addGestureRecognizer:self.revealViewController.panGestureRecognizer];
}
After that, I called Get method in viewDidLoad:
[self GetQUIZ];
Method detail:
- (void)GetQUIZ {
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
NSString *url = [NSString stringWithFormat:#"http://stringlearning.com/api/v1/user-quiz?token=%#",[[NSUserDefaults standardUserDefaults] stringForKey:#"token"]];
[request setURL:[NSURL URLWithString: url]];
[request setHTTPMethod:#"GET"];
[request setValue:#"application/x-www-form-urlencoded" forHTTPHeaderField:#"Content-Type"];
[request setValue:[UIDevice currentDevice].name forHTTPHeaderField:#"device"];
NSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:request delegate:self];
NSLog(#"Left menu, User details: %#", [[NSString alloc] initWithData:[request HTTPBody] encoding:NSUTF8StringEncoding]);
NSLog(#"%#", [request allHTTPHeaderFields]);
if(conn) {
NSLog(#"Connection Successful");
} else
NSLog(#"Connection could not be made");
And then I use data in connectionDidFinishLoading:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSError *deserr = nil;
NSDictionary *responseDict = [NSJSONSerialization JSONObjectWithData:responseData options: 0 error: &deserr];
I read that i should use async methods, but I never use it before. Would you write some detail solution ?
Maybe, does have different path?
I would be very grateful for the help!
I'd suggest starting with NSURLSession, which is a modern API that will accomplish the same thing, asynchronously.
To use NSURLSession, you need a few piece of the puzzle:
A web address to reach, and optionally any payload or custom headers.
An instance of NSURL: where you're downloading from and an NSURLRequest to wrap it in.
An NSURLSessionConfiguration, which handles things like caching, credentials and timeouts.
The session itself.
You need an NSURLSessionTask instance. This is the closest object to your NSURLConnection. It has callbacks via delegate or a completion block, if you just need to know when it finishes.
Here's how this would look in code:
// 1. The web address & headers
NSString *webAddress = [NSString stringWithFormat:#"http://stringlearning.com/api/v1/user-quiz?token=%#",[[NSUserDefaults standardUserDefaults] stringForKey:#"token"]];
NSDictionary <NSString *, NSString *> *headers = #{
#"device" : [UIDevice currentDevice].name,
#"Content-Type" : #"application/x-www-form-urlencoded"
};
// 2. An NSURL wrapped in an NSURLRequest
NSURL* url = [NSURL URLWithString:webAddress];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
// 3. An NSURLSession Configuration
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
[sessionConfiguration setHTTPAdditionalHeaders:headers];
// 4. The URLSession itself.
NSURLSession *urlSession = [NSURLSession sessionWithConfiguration:sessionConfiguration];
// 5. A session task: NSURLSessionDataTask or NSURLSessionDownloadTask
NSURLSessionDataTask *dataTask = [urlSession dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
}];
// 5b. Set the delegate if you did not use the completion handler initializer
// urlSession.delegate = self;
// 6. Finally, call resume on your task.
[dataTask resume];
This will run asynchronously, allowing your UI to remain responsive as your app loads data.
When you send a request on the main thread, like you are doing now, your UI, which is always performed on the main thread, is blocked, waiting for the request to finish and process. So you should perform all your network on a background thread, asynchronously. I would recommend first to check the networking library AFNetworking , it could simplify most of your networking problems.
Welcome to SO. You should know that NSURLConnection was deprecated in iOS 9. You should be using NSURLSession instead. The approach is very similar. You can take the NSURLRequest you've created and pass it to the sharedSession object, which is set up for async requests. The simplest way to deal with it is to use the call dataTaskWithRequest:completionHandler:, which takes a completion block. In your completion block you provide code that handles both success and failure.

How can I achieve a "configurable" NSURLSession shared session?

I want to change the timeout on my HTTP requests. I'm on a project that uses extensively [NSURLSession sharedSession]. I know that I can't change the configuration of that session (it hasn't one at all).
I know I can define a session with my own config (and I can use as baseline [NSURLSessionConfigurationdefaultSessionConfiguration]), but I don't know how similar is this config to the shared one. The shared one has pre-configured cookie storage policies, cache, etc...
TL;DR I want a session exactly equals to sharedSession, but with a larger timeout. How I can achieve that?
Thanks in advance
One option is to not touch the session, but the requests, using either requestWithURL:cachePolicy:timeoutInterval: or by setting timeoutInterval manually (on an NSMutableURLRequest in the latter case, of course).
Otherwise, you can:
copy the configuration of the sharedSession, (using copy), and modify it
create a new session with this modified configuration, and the same delegate and delegate queue.
Something along the lines of:
NSURLSession *sharedSession = [NSURLSession sharedSession];
NSURLSessionConfiguration *configuration = sharedSession.configuration.copy;
configuration.timeoutIntervalForRequest = whatever;
return [NSURLSession sessionWithConfiguration:configuration delegate:sharedSession.delegate delegateQueue:sharedSession.delegateQueue];
(not tested, but you get the idea).
Of course, you would do this in your own singleton class if you want to reuse the same session.
Sample code for NSURLSessionConfiguration
// Instantiate a session configuration object.
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
configuration.timeoutIntervalForRequest = 300;
configuration.HTTPAdditionalHeaders = #{#"Accept" : #"application/xml", #"Content-Type" : #"application/xml; charset=UTF-8", #"User-Agent" : userAgent};
// Instantiate a session object.
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];
// Create a data task object to perform the data downloading.
NSURLSessionDataTask *task = [session dataTaskWithURL:[NSURL URLWithString:"http://myexample.com"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error != nil) {
// If any error occurs then just display its description on the console.
NSLog(#"Server ERROR: \n "
"CODE: %ld, \n "
"LOCALIZED DESCRIPTION: %# \n "
"DESCRIPTION: %# \n "
"FAILURE REASON: %# \n "
"RECOVERY OPTION: %# \n "
"RECOVERY SUGGESTION: %# \n",
(long)[error code],
[error localizedDescription],
error.description,
error.localizedFailureReason,
error.localizedRecoveryOptions,
error.localizedRecoverySuggestion);
}
else {
// If no error occurs, check the HTTP status code.
NSInteger HTTPStatusCode = [(NSHTTPURLResponse *)response statusCode];
if (HTTPStatusCode == 200) {
//Initialize XML parsing with the response data
/*NSString *str = [[NSString alloc]initWithData: data encoding: NSUTF8StringEncoding];
NSLog(#"The URLSession response is: %# \n", str);*/
}
}
}];
// Resume the task.
[task resume];

returning a value from asynchronous call using semaphores

I need to use NSURLSession to make network calls. On the basis of certain things, after I receive the response, I need to return an NSError object.
I am using semaphores to make the asynchronous call behave synchronously.
The problem is, the err is set properly inside call, but as soon as semaphore ends (after
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
), the err becomes nil.
Please help
Code:
-(NSError*)loginWithEmail:(NSString*)email Password:(NSString*)password
{
NSError __block *err = NULL;
// preparing the URL of login
NSURL *Url = [NSURL URLWithString:urlString];
NSData *PostData = [Post dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
// preparing the request object
NSMutableURLRequest *Request = [[NSMutableURLRequest alloc] init];
[Request setURL:Url];
[Request setHTTPMethod:#"POST"];
[Request setValue:postLength forHTTPHeaderField:#"Content-Length"];
[Request setHTTPBody:PostData];
NSMutableDictionary __block *parsedData = NULL; // holds the data after it is parsed
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.TLSMinimumSupportedProtocol = kTLSProtocol11;
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:nil];
NSURLSessionDataTask *task = [session dataTaskWithRequest:Request completionHandler:^(NSData *data, NSURLResponse *response1, NSError *err){
if(!data)
{
err = [NSError errorWithDomain:#"Connection Timeout" code:200 userInfo:nil];
}
else
{
NSString *formattedData = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"%#", formattedData);
if([formattedData rangeOfString:#"<!DOCTYPE"].location != NSNotFound || [formattedData rangeOfString:#"<html"].location != NSNotFound)
{
loginSuccessful = NO;
//*errorr = [NSError errorWithDomain:#"Server Issue" code:201 userInfo:nil];
err = [NSError errorWithDomain:#"Server Issue" code:201 userInfo:nil];
}
else
{
parsedData = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&err];
NSMutableDictionary *dict = [parsedData objectForKey:#"User"];
loginSuccessful = YES;
}
dispatch_semaphore_signal(semaphore);
}];
[task resume];
// but have the thread wait until the task is done
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return err;
}
Rob's answer tells you how to do it right, but not what mistake you made:
You have two variables named err, which are totally unrelated. It seems that you haven't turned on some important warnings, otherwise your code wouldn't even have compiled.
The parameter err that is passed to your completion block is the error from the URL request. You replace it without thinking with a timeout error - so the true error is now lost. Consider that timeout is not the only error.
But all the errors that you set only set the local variable err which was passed to you in the completion block; they never touch the variable err in the caller at all.
PS. Several serious errors in your JSON handling. JSON can come in UTF-16 or UTF-32, in which case formattedData will be nil and you incorrectly print "Server Issue". If the data isn't JSON there is no guarantee that it contains DOCTYPE or html, that test is absolute rubbish. Your user with the nickname JoeSmith will hate you.
Passing NSJSONReadingAllowFragments to NSJSONSerialization is nonsense. dict is not mutable; if you try to modify it your app will crash. You don't check that the parser returned a dictionary, you don't check that there is a value for the key "User", and you don't check that the value is a dictionary. That's lots of ways how your app can crash.
I would suggest cutting the Gordian knot: You should not use semaphores to make an asynchronous method behave synchronously. Adopt asynchronous patterns, e.g. use a completion handler:
- (void)loginWithEmail:(NSString *)email password:(NSString*)password completionHandler:(void (^ __nonnull)(NSDictionary *userDictionary, NSError *error))completionHandler
{
NSString *post = ...; // build your `post` here, making sure to percent-escape userid and password if this is x-www-form-urlencoded request
NSURL *url = [NSURL URLWithString:urlString];
NSData *postData = [post dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:#"POST"];
// [request setValue:postLength forHTTPHeaderField:#"Content-Length"]; // not needed to set length ... this is done for you
[request setValue:#"application/x-www-form-urlencoded" forHTTPHeaderField:#"Content-Type"]; // but it is best practice to set the `Content-Type`; use whatever `Content-Type` appropriate for your request
[request setValue:#"text/json" forHTTPHeaderField:#"Accept"]; // and it's also best practice to also inform server of what sort of response you'll accept
[request setHTTPBody:postData];
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.TLSMinimumSupportedProtocol = kTLSProtocol11;
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:nil];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *err) {
if (!data) {
dispatch_async(dispatch_get_main_queue(), ^{
completionHandler(nil, [NSError errorWithDomain:#"Connection Timeout" code:200 userInfo:nil]);
});
} else {
NSError *parseError;
NSDictionary *parsedData = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&parseError];
dispatch_async(dispatch_get_main_queue(), ^{
if (parsedData) {
NSDictionary *dict = parsedData[#"User"];
completionHandler(dict, nil);
} else {
completionHandler(nil, [NSError errorWithDomain:#"Server Issue" code:201 userInfo:nil]);
}
});
}
}];
[task resume];
}
And then call it like so:
[self loginWithEmail:userid password:password completionHandler:^(NSDictionary *userDictionary, NSError *error) {
if (error) {
// do whatever you want on error here
} else {
// successful, use `userDictionary` here
}
}];
// but don't do anything reliant on successful login here; put it inside the block above
Note:
I know you're going to object to restoring this back to asynchronous method, but it's a really bad idea to make this synchronous. First it's a horrible UX (the app will freeze and the user won't know if it's really doing something or whether it's dead) and if you're on a slow network you can have all sorts of problems (e.g. the watchdog process can kill your app if you do this at the wrong time).
So, keep this asynchronous. Ideally, show UIActivityIndicatorView before starting asynchronous login, and turn it off in the completionHandler. The completionHandler would also initiate the next step in the process (e.g. performSegueWithIdentifier).
I don't bother testing for HTML content; it is easier to just attempt parse JSON and see if it succeeds or not. You'll also capture a broader array of errors this way.
Personally, I wouldn't return my own error objects. I'd just go ahead and return the error objects the OS gave to me. That way, if the caller had to differentiate between different error codes (e.g. no connection vs server error), you could.
And if you use your own error codes, I'd suggest not varying the domain. The domain should cover a whole category of errors (e.g. perhaps one custom domain for all of your app's own internal errors), not vary from one error to another. It's not good practice to use the domain field for something like error messages. If you want something more descriptive in your NSError object, put the text of the error message inside the userInfo dictionary.
I might suggest method/variable names to conform to Cocoa naming conventions (e.g. classes start with uppercase letter, variables and method names and parameters start with lowercase letter).
There's no need to set Content-Length (that's done for you), but it is good practice to set Content-Type and Accept (though not necessary).
You need to let the compiler know that you will be modifying err. It needs some special handling to preserve that beyond the life of the block. Declare it with __block:
__block NSError *err = NULL;
See Blocks and Variables in Blocks Programming Topics for more details.

sendAsynchronousRequest:queue:completionHandler: With Delegate Methods

I have a server with a self-signed SSL certificate installed. However, once I call the following method it doesn't get any response. Once I change the URL back to http, it works.
- (void)getAccountInfoWithCompletion:(void (^)(NSDictionary *json_response, NSError *error))completion
{
NSURLRequest *request = [NSURLRequest requestWithURL:
[NSURL URLWithString:
[NSString stringWithFormat:#"%#/api/account/%d/get_info", BASE_HOST_URL_IP, [self getUserID]]
]];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (error)
{
if (completion)
{
//completion(#"error", error);
}
} else {
NSString *response_string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSDictionary *json_object = [NSJSONSerialization JSONObjectWithData:[response_string dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
if (completion)
{
completion(json_object, error);
}
}
}];
}
My reason for the delegate is so I can use the self-signed certificate in my app. The following tutorial is what I was using, but then I realized I couldn't use the delegate with the completionHandler method. I need to keep the completionHandler method though.
http://www.cocoanetics.com/2010/12/nsurlconnection-with-self-signed-certificates/
What could I do in order to receive a response from the SSL site?
In the case you describe, you (pretty much) have to use a delegate.
What's happening here is that sendAsynchronousRequest:queue:completion: uses the default behavior of the URL loading system. The URL loading system sees your self signed certificate, can't verify it, so it can't trust it - and won't connect. You should see the NSError passed in to the completion handler populated with information about the problem.
This is all described in depth in Technote 2232: HTTPS Server Trust Evaluation
To allow your self signed certificate, you can't use sendAsynchronousRequest:queue:completion: unless you have a way to make your self-signed certificate trusted and stored in the keychain - on iOS this is only practical in managed devices. For testing, and ONLY for testing, you can use a private Apple API to alter the default trust behavior.
For production code, you must implement a NSURLConnectionDelegate that handles evaluating the server provided credentials and allows your self-signed certificate. This is also described (at a high level) in Technote 2232. If you do not implement this correctly you may create a security vulnerability in your app - and that would be bad, mmmmmk?
I would not suggest following the guidance of the Cocoanetics post you reference. The material is outdated and of questionable quality. Refer to the documentation for NSURLConnectionDelegate and the mentioned Technote 2232 instead. If you would like more information on transport level security for mobile applications in general, there are plenty of resources available.
If you STILL want to use a self signed certificate, you can implement SSL public key pinning to match the remote (self signed) public key against a known local value stored inside your application. This is much better than attempting to match just the hostname. Some example code to get you started is here
ViewController.h:
#interface ViewController : UIViewController <NSURLSessionDelegate>
#end
ViewController.m:
- (void)getAccountInfoWithCompletion:(void (^)(NSDictionary *json_response, NSError *error))completion
{
NSURLRequest *request = [NSURLRequest requestWithURL:
[NSURL URLWithString:
[NSString stringWithFormat:#"%#/api/account/%d/get_info", BASE_HOST_URL_IP, [self getUserID]]
]];
NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration ephemeralSessionConfiguration];
defaultConfigObject.requestCachePolicy = NSURLRequestReloadIgnoringLocalAndRemoteCacheData;
NSURLSession *defaultSession = [NSURLSession sessionWithConfiguration:defaultConfigObject delegate:self delegateQueue: [NSOperationQueue mainQueue]];
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
NSURLSessionDataTask *dataTask = [defaultSession dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
{
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
if (error == nil && data != nil)
{
NSString *response_string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSDictionary *json_object = [NSJSONSerialization JSONObjectWithData:[response_string dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
if (completion)
{
completion(json_object, error);
}
}
}];
[dataTask resume];
}
The new beautiful delegate method which lets us replace NSURLConnection's sendAsynchronousRequest method (which couldn't handle SSL)
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler
{
NSString *host = challenge.protectionSpace.host;
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust])
{
if ([host rangeOfString:#"yourHost.net"].location != NSNotFound)
{
completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]);
}
else
{
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge,nil);
}
}
}

Resources