51
votes

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?

7
That's basically how you do it, there's no cleaner way without dropping down to [AFURLSessionManager dataTaskWithRequest:completionHandler:] which gets passed the same NSURLResponse (and you have to cast it there, too)David Snabel-Caunt
That answers how to get the statusCode. Unfortunately NSHTTPURLResponse does not contain the body/data in the failure block :(Oleksandr
It looks like the response is available in the notification userInfo key AFNetworkingTaskDidFinishResponseDataKey if you observer the task's AFNetworkingTaskDidFinishNotification notification. If that helps I'll write it up as an answer below.David Snabel-Caunt
If that's the only solution available, excepting subclassing the manager then why not… Man, the perfectly working AFHTTPClient was trashed because of conceptual weirdness!Oleksandr
You could raise an issue on GitHub and ask that the response data is passed in the error's userInfo dictionary. It would certainly be cleaner!David Snabel-Caunt

7 Answers

68
votes

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 ); 
13
votes

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

12
votes

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;}
4
votes

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

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


     }];
2
votes

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
    }
}
0
votes

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