Retrieving Response Body via AFNetworking with an HTTP Error Status Code

I use AFNetworking as a convenience layer to the NSURLSession classes in most iOS apps that would traditionally have a non-trivial amounts of networking code. I tend to not like using much third-party code, but the way Mattt Thompson has structured AFNetworking and the great community backing the project have led me to make an exception. AFNetworking makes it trivial to interact with the now-common JSON and XML-based APIs right out of the box.

There are several schools of thought regarding transmitting error message via web APIs. Many APIs simply return an error HTTP status code (4XX, 5XX) in the header of the response. Others will return a successful status code, but the body of the response will contain an error message that the client must parse. Others, like Foursquare and Untappd, use a hybrid approach — an HTTP error status code is set in the header and the body contains a JSON object with the error message. In AFNetworking 1.x  (in the NSURLConnection classes), the NSError parameter of the failure block of AFHTTPClient convenience methods would report the body of the failed response in their userInfo property’s NSLocalizedRecoverySuggestionErrorKey key. This could be considered a bit of an abuse of NSError, but it worked well. As of 2.x (in the NSURLSession classes), this functionality was not implemented and there is no way out of the box to retrieve error messages sent from a web service via AFNetworking.

The good news is there’s a trivial way to get this functionality back that doesn’t require edits to the upstream code. I mentioned I especially liked how AFNetworking was structured, and this is one of the ways. AFNetworking 2.x introduces the concept of custom serializers for data returned from a web service. In 1.x, you received an NSData object and did with it whatever you wanted in the success block. You’d traditionally do things like check the length, check the status code, serialize JSON/parse XML, etc. 2.x removes a lot of this boilerplate so you can get right to interacting with the received data. If you’re expecting JSON, you’d attach a JSON serializer to your manager. When you have a successful response from the web service, you’d get back your NSDictionary of parsed JSON data in your success block. No muss, no fuss.

Back to retrieving error messages from the body of a response with an HTTP error status code in the header. You can simply subclass the existing serializer, embed the error message in a new NSError object at the point where AFNetworking would discover the error condition, and then pass control back to AFNetworking proper. I detailed this method over on GitHub a few months ago, but I thought re-posting a cleaned up version here might be a good home for the solution.

JSONResponseSerializerWithData.h:

#import "AFURLResponseSerialization.h"
 
/// NSError userInfo key that will contain response data
static NSString * const JSONResponseSerializerWithDataKey = @"JSONResponseSerializerWithDataKey";
 
@interface JSONResponseSerializerWithData : AFJSONResponseSerializer
@end

JSONResponseSerializerWithData.m:

#import "JSONResponseSerializerWithData.h"
 
@implementation JSONResponseSerializerWithData
 
- (id)responseObjectForResponse:(NSURLResponse *)response
                           data:(NSData *)data
                          error:(NSError *__autoreleasing *)error
{
	id JSONObject = [super responseObjectForResponse:response data:data error:error];
	if (*error != nil) {
		NSMutableDictionary *userInfo = [(*error).userInfo mutableCopy];
		if (data == nil) {
//			// NOTE: You might want to convert data to a string here too, up to you.
//			userInfo[JSONResponseSerializerWithDataKey] = @"";
			userInfo[JSONResponseSerializerWithDataKey] = [NSData data];
		} else {
//			// NOTE: You might want to convert data to a string here too, up to you.
//			userInfo[JSONResponseSerializerWithDataKey] = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
			userInfo[JSONResponseSerializerWithDataKey] = data;
		}
		NSError *newError = [NSError errorWithDomain:(*error).domain code:(*error).code userInfo:userInfo];
		(*error) = newError;
	}
 
	return (JSONObject);
}
 
@end

You probably have a custom AFHTTPSessionManager somewhere, you just need to set the responseSerializer property on it.

CustomSessionManager.m:

+ (instancetype)sharedManager
{
	static CustomSharedManager *manager = nil;
	static dispatch_once_t onceToken;
	dispatch_once(&onceToken, ^{
		manager = [[CustomSharedManager alloc] initWithBaseURL:<# your base URL #>];
 
		// *** Use our custom response serializer ***
		manager.responseSerializer = [JSONResponseSerializerWithData serializer];
	});
 
	return (manager);
}

Your serializer will now capture the error message and embed it in NSError object you are used to seeing in the failure block of your AFHTTPSessionManager‘s GET/POST/etc. convenience methods, where you can manipulate it. If you converted it to a string above, you could print it in a UIAlertView, etc.

15 thoughts on “Retrieving Response Body via AFNetworking with an HTTP Error Status Code

  1. I had to do this in the finish blocks, user info didn’t have any status code :
    . . .
    failure:^(NSURLSessionDataTask *task, NSError *error) {
    DLog(@”\n============== ERROR ====\n%@”,error.userInfo);
    NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response;
    int statuscode = response.statusCode;

  2. dealing with a json api i found i needed to still deserialize the NSData object into a dictionary, via:

    NSDictionary *rd = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:error];
    userInfo[JSONResponseSerializerWithDataKey] =rd;

  3. This is great! Saved me a lot of time. Thanks. Maybe you can add the line how to NSJSONSerialization the data into a readable dictionary in your post.

  4. You can access the “data” object directly from AFNetworking by using the “AFNetworkingOperationFailingURLResponseDataErrorKey” key so there is no need for subclassing the AFJSONResponseSerializer. You can the serialize the data into a readable dictionary.
    Here is some sample code :
    NSData *errorData = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey];
    NSDictionary *serializedData = [NSJSONSerialization JSONObjectWithData: errorData options:kNilOptions error:nil];

Leave a Reply

Your email address will not be published. Required fields are marked *