AFNetworking 2.0 AFHTTPSessionManager: how to get status code and response JSON in failure block?

Objective CAfnetworking 2

Objective C Problem Overview


When switched to AFNetworking 2.0 the AFHTTPClient has been replaced by AFHTTPRequestOperationManager / AFHTTPSessionManager (as mentioned in the migration guide). The very first issue I came across when using the AFHTTPSessionManager is how to retrieve the body of the response in the failure block?

Here's an example:

[self.sessionManager POST:[endpoint absoluteString] parameters:params success:^(NSURLSessionDataTask *task, id responseObject) {
    // How to get the status code?
} failure:^(NSURLSessionDataTask *task, NSError *error) {
    // How to get the status code? response?
}];

In the success block I would like to retrieve response's status code. In the failure block I would like to retrieve both response's status code and the content (which is JSON in this case that describes the server-side error).

The NSURLSessionDataTask has a response property of type NSURLResponse, which has not statusCode field. Currently I'm able to retrieve statusCode like this:

[self.sessionManager POST:[endpoint absoluteString] parameters:params success:^(NSURLSessionDataTask *task, id responseObject) {
    // How to get the status code?
} failure:^(NSURLSessionDataTask *task, NSError *error) {
    NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response;
    DDLogError(@"Response statusCode: %i", response.statusCode);

}];

But this looks ugly to me. And still can't figure about the response's body.

Any suggestions?

Objective C Solutions


Solution 1 - Objective C

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 to get JSON Data :

 NSData *errorData = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey];
 NSDictionary *serializedData = [NSJSONSerialization JSONObjectWithData: errorData options:kNilOptions error:nil];

Here is the code to Get Status code in the Failure block:

  NSHTTPURLResponse* r = (NSHTTPURLResponse*)task.response;
  NSLog( @"success: %d", r.statusCode ); 

Solution 2 - Objective C

After of read and research for several days, It worked for me:

  1. You have to build your own subclass of AFJSONResponseSerializer

File : JSONResponseSerializerWithData.h:

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

File: 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);
}

2) Setup your own JSONResponseSerializer in your AFHTTPSessionManager

+ (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);
}

> Source: http://blog.gregfiumara.com/archives/239

Solution 3 - Objective C

You can get the status code like this, read the failure block...

 NSURLSessionDataTask *op = [[IAClient sharedClient] POST:path parameters:paramsDict constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
    } success:^(NSURLSessionDataTask *task, id responseObject) {
        DLog(@"\n============= Entity Saved Success ===\n%@",responseObject);
        
        completionBlock(responseObject, nil);
    } failure:^(NSURLSessionDataTask *task, NSError *error) {
        DLog(@"\n============== ERROR ====\n%@",error.userInfo);
        NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response;
        int statuscode = response.statusCode;}

Solution 4 - Objective C

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];

Solution 5 - Objective C

There is another approach besides the accepted answer.

AFNetworking is calling your failure block, sans any response object, because it believes a true failure has occurred (e.g. a HTTP 404 response, perhaps). The reason it interprets 404 as an error is because 404 isn't in the set of "acceptable status codes" owned by the response serializer (the default range of acceptable codes is 200-299). If you add 404 (or 400, or 500, or whatever) to that set then a response with that code will be deemed acceptable and will be routed to your success block instead - complete with the decoded response object.

But 404 is an error! I want my failure block to be called for errors! If that's the case then use the solution referred to by the accepted answer: https://github.com/AFNetworking/AFNetworking/issues/1397. But consider that perhaps a 404 is really a success if you're going to be extracting and processing the content. In this case your failure block handles real failures - e.g. unresolvable domains, network timeouts, etc. You can easily retrieve the status code in your success block and process accordingly.

Now I understand - it might be super nice if AFNetworking passed any responseObject to the failure block. But it doesn't.

    _sm = [[AFHTTPSessionManager alloc] initWithBaseURL: [NSURL URLWithString: @"http://www.stackoverflow.com" ]];

    _sm.responseSerializer = [AFHTTPResponseSerializer new];
    _sm.responseSerializer.acceptableContentTypes = nil;
    
    NSMutableIndexSet* codes = [NSMutableIndexSet indexSetWithIndexesInRange: NSMakeRange(200, 100)];
    [codes addIndex: 404];
    

    _sm.responseSerializer.acceptableStatusCodes = codes;
    
    [_sm GET: @"doesnt_exist"
  parameters: nil success:^(NSURLSessionDataTask *task, id responseObject) {
      
      NSHTTPURLResponse* r = (NSHTTPURLResponse*)task.response;
      
      NSLog( @"success: %d", r.statusCode );

      NSString* s = [[NSString alloc] initWithData: responseObject encoding:NSUTF8StringEncoding];
      
      NSLog( @"%@", s );

  }
     failure:^(NSURLSessionDataTask *task, NSError *error) {
    
         NSLog( @"fail: %@", error );
    

     }];

Solution 6 - Objective C

In Swift 2.0 (in case you cannot use Alamofire yet):

Get status code:

if let response = error.userInfo[AFNetworkingOperationFailingURLResponseErrorKey] as? NSHTTPURLResponse {
    print(response.statusCode)
}

Get response data:

if let data = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] as? NSData {
    print("\(data.length)")
}

Some JSON REST APIs return error messages in their error responses (Amazon AWS services for example). I use this function to extract the error message from an NSError that has been thrown by AFNetworking:

// Example: Returns string "error123" for JSON { message: "error123" }
func responseMessageFromError(error: NSError) -> String? {
    do {
        guard let data = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] as? NSData else {
            return nil
        }
        guard let json = try NSJSONSerialization.JSONObjectWithData(data, options: .AllowFragments) as? [String: String] else {
            return nil
        }
        if let message = json["message"] {
            return message
        }
        return nil
    } catch {
        return nil
    }
}

Solution 7 - Objective C

You can get the userInfo dictionary associated with the NSError object and traverse down that to get the exact response you need. For example in my case I get an error from a server and I can see the userInfo like in ScreeShot

ScreenShot displaying AFNetworking error

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionOleksandrView Question on Stackoverflow
Solution 1 - Objective CpythonView Answer on Stackoverflow
Solution 2 - Objective CshontauroView Answer on Stackoverflow
Solution 3 - Objective ComarojoView Answer on Stackoverflow
Solution 4 - Objective CpythonView Answer on Stackoverflow
Solution 5 - Objective CTomSwiftView Answer on Stackoverflow
Solution 6 - Objective CsebView Answer on Stackoverflow
Solution 7 - Objective CunspokenblabberView Answer on Stackoverflow