Testing use of NSURLConnection with HTTP response error statuses - iphone

I'm writing an iPhone application that needs to get some data from a web server. I'm using NSURLConnection to do the HTTP request, which works well, but I'm having trouble unit testing my code in the case where the response has an HTTP error code (like 404 or 500).
I'm using GTM for unit testing and OCMock for mocking.
When the server returns an error, the connection does not call connection:didFailWithError: on the delegate, but calls connection:didReceiveResponse:, connection:didReceiveData:, and connectionDidFinishLoading: instead. I'm currently checking the status code on the response in connection:didReceiveResponse: and calling cancel on the connection when the status code looks like an error to prevent connectionDidFinishLoading: from being called, where a successful response would be reported.
Providing a static stubbed NSURLConnection is simple, but I want my test to change it's behaviour when one of the mock connection's methods is called. Specifically, I want the test to be able to tell when the code has called cancel on the mock connection, so the test can stop calling connection:didReceiveData: and connectionDidFinishLoading: on the delegate.
Is there a way for tests to tell if cancel has been called on the mock object? Is there a better way to test code that uses NSURLConnection? Is there a better way to handle HTTP error statuses?

Is there a better way to handle HTTP error statuses?
I think you are on the right track. I use something similar to the following code, which I found here:
if ([response respondsToSelector:#selector(statusCode)])
{
int statusCode = [((NSHTTPURLResponse *)response) statusCode];
if (statusCode >= 400)
{
[connection cancel]; // stop connecting; no more delegate messages
NSDictionary *errorInfo
= [NSDictionary dictionaryWithObject:[NSString stringWithFormat:
NSLocalizedString(#"Server returned status code %d",#""),
statusCode]
forKey:NSLocalizedDescriptionKey];
NSError *statusError
= [NSError errorWithDomain:NSHTTPPropertyStatusCodeKey
code:statusCode
userInfo:errorInfo];
[self connection:connection didFailWithError:statusError];
}
}
This cancels the connection, and calls connection:didFailWithError: in order to make http error codes behave exactly the same as any other connection error.

Related

AFNetworking and Network Errors

I am using the AFNetworking framework to make several JSON web request.
During development if I fail to provide a required parameters or the Service developers have broken something :-) i receive a 500 error
The Error block of AFJSONRequestOperation is correctly catching it. However i cannot see the page body because AfNetworking is cancelling the request as soon as the error code in the head is received.
Is there some work around. I would like to spit out the response body to the log.
Use the responseData or responseString property from the operation that's passed into the failure block.
You'll need to add the http Code 500 to the acceptable status codes and filter it on the success callback. There you will have a responseObject.

Preventing ASIHTTPRequest from following HTTP 303 See Other

I'm writing an iPhone Application using the ASIHTTPRequest (http://allseeing-i.com/ASIHTTPRequest/) library for REST interactions to a Web App's RESTful services.
I am currently facing a bug where I am receiving a 200 OK from one of these pages and there is no body. Additionally, I noticed that the headers are blank after the request has been completed, but the headers were not blank beforehand (they were filled with an OAuth Authentication).
Through an over-complicated network / proxy setup, I was able to verify that I am receiving a 303 (as expected) and that ASIHTTPRequest is following that request (as semi-expected). However, since I need to recreate my OAuth authentication headers so that the RESTful services will give me the information I want, I need to prevent ASIHTTPRequest from following 303s, and instead to just return the 303 so I can read the Location header myself and create a new request with the appropriate OAuth Headers.
If anybody has had to do this, please let me know how you did it!
Thanks,
Tyler
Dives off the diving board into the NSAutoreleasePool
You can check your HTTP status in request:didReceiveResponseHeaders: delegate method. Here's the code:
- (void)request:(ASIHTTPRequest *)request didReceiveResponseHeaders:(NSDictionary *)responseHeaders {
if(request.responseStatusCode == 303) {
[request cancel];
}
}
- (void)requestFailed:(ASIHTTPRequest *)request {
if(request.responseStatusCode == 303) {
// here you can call your custom methods
}
}
It will cancel your connection as soon as it receives HTTP headers, so it will not load the full page.

NSURLConnection requests from multiple methods - getting a callback to starter method

I have a few methods that are posting data to a server. I am using NSURLConnection asynchronously. I am using a class variable NSMutableData and this is created in the viewDidLoad.
I have one method connection created for login and one for registration. Both just return a success booleon in response.
My question is, when the connection is complete and I receive my response, is there anyway to know which method started the connection? I.e some kind of callback to say that the login method started the connection.
Thanks
When you start the connection, store a reference to the NSURLConnection object. When the connection finishes it will pass you the connection object as a parameter, which you can check like so:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
if (connection==loginConnection) {
} else if (connection==regConnection) {
}
}
The other (possibly better) option is to offload all of the HTTP connection handling to a separate wrapper class. You can build your own from your existing code, or use a third-party one like ASIHTTPRequest or GTM HTTP Fetcher.

When does NSURLConnection's initWithRequest return nil

Does anyone know in which situations initializing a NSURLConnection returns nil instead of the created connection. The documentation says it's possible but fails to specify when this happens.
The method/message in question:
[[NSURLConnection alloc] initWithRequest:request delegate:self];
-
According to the NSURLConnection Class Reference:
Return Value: The URL connection for the URL request. Returns nil if a connection can't be initialized.
The URL Loading System Programming Guide says the following:
If NSURLConnection can’t create a connection for the request, initWithRequest:delegate: returns nil.
-
While it's possible that this method returns nil, I'm unable to come up with a scenario which triggers this. I've tried the following scenarios:
URLRequest with an empty url: connection:didFailWithError: delegate method is called with "unsupported URL" as error.
URLRequest with invalid url: connection:didFailWithError: delegate method is called with "bad URL" as error.
URLRequest with nonexistent url: connection:didFailWithError: delegate method is called with "A server with the specified hostname could not be found." as error.
Valid request but no internet: connection:didFailWithError: delegate method is called with "The Internet connection appears to be offline." as error.
nil request: causes a crash.
The initWithRequest method returned a valid NSURLConnection in each scenario (besides the last one) and called the connection:didFailWithError: with an appropriate error.
Has anybody been able to figure out which scenario does cause nil to be returned?
I believe this can also be used when it fails to load, not just initialize. (The alloc is done separately - that's where low mem would probably bite you) So (I'm guessing) it could fail because you did not have a network available (3G/Wifi) or it simply failed to connect to the server. In any event, use:
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
To get the actual failure.
You may be able to simulate this with an absence of a network - or even giving it a bad URL.
I guess the answer is "Never". Seems only way for NSURLConnection to return nil is failing at [super init]. ([super init] returning nil) But as super class of NSURLConnection is NSObject and NSObjects init just returns self (never nil)
PS: That's for IOS SDK 4.0, on emulator, can be different on device.
I would try all of the above except do it during low memory conditions. IE, I think it will happen when an internal malloc fails.
I thing this is caused if the request u specified has wrong or not all essential values
NSURLConnection returns nil if it is not created inside a runLoop.

ASIFormDataRequest- POST of file continues seems to continue after authentication fails

I'm posting some data to a http authenticated url with ASIFormDataRequest.
When the authentication fails and the authentication dialog delegate is invoked the upload progress seems to still proceed fully.
So in these cases:
1) The user's credentials are not yet stored in the keychain
2) The user's credentials which are stored in the keychain fail authentication (expired etc.)
I see this behavior:
I see the request come in to my
server and the 401 denied error
returned to the client
The uploadFailed delegate is not
called.
Progress bar delegate slowly fills as
the file appears to still be pushed
out on the network connection. It
completes in a time consistent with
the amount of time to fully upload
The built in authentication dialog
modal appears
User enters correct credentials
Progress bar delegate resets
Upload begins again - progress bar
fills as post data is received on
server
Finished delegate method is called as
expected.
Everything has uploaded just fine
with this second attempt
Here's where I setup my operation:
[self setRequest:[ASIFormDataRequest requestWithURL:uploadURL]];
[request setDelegate:self];
[request setDidFailSelector:#selector(uploadFailed:)];
[request setDidFinishSelector:#selector(uploadFinished:)];
[request setUseKeychainPersistence:TRUE];
[request setShouldPresentAuthenticationDialog:TRUE];
[request setShouldPresentCredentialsBeforeChallenge:TRUE];
[request setPostValue:captionTextField.text forKey:#"caption"];
[request setPostValue:[siteID stringValue] forKey:#"site_id"];
[request setFile:fileToUpload forKey:#"site_photo"];
[request setUploadProgressDelegate:progressView];
[request startAsynchronous];
I am thinking I need to issue a [request cancel] upon the authentication failing but I'm not sure where I should be doing this.
Is it expected behavior that the POST will still chug away even after the server has returned a 401?
Appreciate any guidance or pointers to existing questions that address this.
A 401 "error" is an HTTP status code, not a request failure. Your request went through okay and you got a response, which happens to be an authentication error notice. You are responsible for handling the response, whatever it might be.
There are many possible status codes you can get from a successful request, other than 401. As an aside, you may want to think about how to handle those kinds of responses as well, depending on what the end user is doing and what responses are appropriate.
The method -uploadFinished: should not generally be waiting until the data is fully uploaded before you see any NSLog statements or other notification of the request finishing.
So one thing to do is change the -uploadFailed: and -uploadFinished: method names to -requestFailed: and -requestFinished: to more accurately reflect what is happening in the logic of your application.
In your delegate's -requestFinished: method, check the responseStatusCode property of the request and issue the appropriate commands:
- (void) requestFinished:(ASIHTTPRequest *)request {
if ([request responseStatusCode] == 401) {
//
// cancel the current request and/or trigger a new,
// separate authentication request, passing along a
// reference to the request's body data, etc.
//
}
}
This is fairly common behaviour for HTTP clients - they do not attempt to read the reply from the server till they have fully sent the request, including the attached file.
It's common behaviour for a client to pre-emptively send the authentication if it has already has a request from the same server rejected with a 401 with in the same session - I am unsure if ASIHTTPRequest does this, but if it does one solution would be to make a GET request to the server before you do the POST. If the GET is successfully authenticated then the cached credentials should be sent for the post and hence there won't be a 401 error.
The only other option I can think of would be to move to cookie based authentication instead, if you are in control of the server, or use authentication in a custom http header. But I think my suggestion of doing a GET request first may be the best approach.