A common issue I've encountered with AFNetworking is that, unless you delve into the internals, you get no straightforward way of accessing the response body in the failure callbacks. Now this can be a big issue when your iPhone app is talking to an API that sends you errors with 400 or 500 responses, and you're interested in grabbing the actual error message (or other details) from the response. There are numerous solutions to this, but most of them involve giving up on AFNetworking's convenience methods, which (for me) really is a shame. This solution inserts the response into the NSError that is returned, for minimal impact to the way you deal with AFNetworking 2, in a similar way as used to work with AFNetworking 1.

All told, here is how you would use my solution, if you apply my steps further down this post:

[[MySessionManager sharedManager] POST:@"my-api" parameters:myParams success:^(NSURLSessionDataTask *task, id responseObject) {
    ...
} failure:^(NSURLSessionDataTask *task, NSError *error) {
    id responseObject = error.userInfo[kErrorResponseObjectKey];
    ... great! You've got the response for an error now ...
}];

AFHTTPSessionManager subclass

First for a bit of groundwork. I've found it to be a good practice to subclass AFHTTPSessionManager, and manage your session manager singleton in said subclass. This subclass will come in handy later for capturing the error response bodies. So lets get started with the below:

//  MySessionManager.h

#import "AFHTTPSessionManager.h"

#define kErrorResponseObjectKey @"kErrorResponseObjectKey"

@interface MySessionManager : AFHTTPSessionManager

/// The singleton. Use this to talk to your API.
+ (instancetype)sharedManager;

@end

...

//  MySessionManager.m

#import "MySessionManager.h"

#if DEBUG
    static NSString *kBaseUrl = @"http://testapi.myapp.com/v1/";
#else
    static NSString *kBaseUrl = @"https://api.myapp.com/v1/";
#endif

@implementation MySessionManager

+ (instancetype)sharedManager {
    static id instance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] initWithBaseURL:[NSURL URLWithString:kBaseUrl]];
    });
    return instance;
}

@end

Tip: You could use initWithBaseURL:sessionConfiguration: above if you'd like to customise your configuration, eg have a common HTTP header that you'd like to send with all your API requests.

To use the above singleton to access your API's (this will not capture errors yet, but it gives you the idea), you can do the below:

[[MySessionManager sharedManager] GET:@"some-endpoint" parameters:myParams success:^(NSURLSessionDataTask *task, id responseObject) {
    ...
} failure:^(NSURLSessionDataTask *task, NSError *error) {
    ...
}];

Which, in itself, is quite a neat and tidy way of using AFNetworking 2. I hope the above, in and of itself, is useful for you :)

Grabbing the errors

Next, we want to override dataTaskWithRequest:completionHandler: in the subclass, so that it wraps the completion handler with a 'shim' which puts the response body into the NSError, before calling the original completion handler. Here it is below:

//  MySessionManager.m continued...

/// This wraps the completion handler with a shim that injects the responseObject into the error.
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request
                            completionHandler:(void (^)(NSURLResponse *, id, NSError *))originalCompletionHandler {
    return [super dataTaskWithRequest:request
                    completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {

        // If there's an error, store the response in it if we've got one.
        if (error && responseObject) {
            if (error.userInfo) { // Already has a dictionary, so we need to add to it.
                NSMutableDictionary *userInfo = [error.userInfo mutableCopy];
                userInfo[kErrorResponseObjectKey] = responseObject;
                error = [NSError errorWithDomain:error.domain
                                            code:error.code
                                        userInfo:[userInfo copy]];
            } else { // No dictionary, make a new one.
                error = [NSError errorWithDomain:error.domain
                                            code:error.code
                                        userInfo:@{kErrorResponseObjectKey: responseObject}];
            }
        }

        // Call the original handler.
        if (originalCompletionHandler) {
            originalCompletionHandler(response, responseObject, error);
        }
    }];
}

And that's it! Your failure callbacks will now be able to access errors as described at the start of this article, as follows:

id responseObject = error.userInfo[kErrorResponseObjectKey];

Hope it's helpful :)

Thanks for reading! And if you want to get in touch, I'd love to hear from you: chris.hulbert at gmail.

Chris Hulbert

(Comp Sci, Hons - UTS)

iOS Developer in Sydney.

I have worked at places such as Google, News Corp, Fox Sports, NineMSN, FetchTV, Woolworths, and Westpac, among others. If you're looking for a good iOS developer, drop me a line!

Get in touch:
chris.hulbert@gmail.com
github.com/chrishulbert
linkedin
my resume



 Subscribe via RSS