52

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?

Oleksandr
  • 1,001
  • 1
  • 11
  • 14
  • 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 Sep 30 '13 at 14:47
  • That answers how to get the statusCode. Unfortunately NSHTTPURLResponse does not contain the body/data in the failure block :( – Oleksandr Sep 30 '13 at 15:01
  • 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 Sep 30 '13 at 15:16
  • 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 Sep 30 '13 at 15:35
  • 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 Sep 30 '13 at 15:40
  • Will do that! Thanks for the suggestion! – Oleksandr Sep 30 '13 at 15:57
  • https://github.com/AFNetworking/AFNetworking/issues/1397 – Oleksandr Sep 30 '13 at 15:59

7 Answers7

68

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 ); 
tjpaul
  • 343
  • 3
  • 16
python
  • 1,407
  • 1
  • 15
  • 31
  • 2
    Thanks for the pointer! It should be noted, however, that this was a relatively new addition (AFNetworking 2.4.0, released on 2014-09-03), thus not available at the time the question was asked. @Olx: This should now be the accepted answer! – Daniel Rinser Feb 26 '15 at 10:15
13

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

shontauro
  • 1,812
  • 1
  • 25
  • 26
12

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;}
omarojo
  • 1,197
  • 1
  • 13
  • 26
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];
python
  • 1,407
  • 1
  • 15
  • 31
  • 1
    You should probably delete this duplicate answer... When adding something to your answer, please edit the existing one rather than creating a new one. – Daniel Rinser Feb 26 '15 at 10:17
  • That answer is great, if you need the response string you can use `NSString *errString = [[NSString alloc] initWithData:errorData encoding:NSUTF8StringEncoding];` – Johnny Aug 31 '16 at 12:18
3

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


     }];
TomSwift
  • 39,369
  • 12
  • 121
  • 149
2

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
    }
}
seb
  • 2,350
  • 24
  • 30
0

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

unspokenblabber
  • 1,567
  • 1
  • 11
  • 19
  • 1
    In my case it doesn't provide such a detailed userInfo as you can see from the screenshot below. [Example Screenshot](https://dl.dropboxusercontent.com/u/7487971/Screen%20Shot%202013-10-01%20at%2012.53.07%20PM.png). – Oleksandr Oct 01 '13 at 10:54
  • That would mean that the server is not adding the needed content/data in it's failure response body. I suggest you ask the server-side developer to give you the desired data in its response body – unspokenblabber Oct 01 '13 at 22:00
  • the server returns the correct response body. I can see it in AFNetworkingTaskDidFinishNotification (for debugging reasons). – Oleksandr Oct 02 '13 at 07:59
  • unspokenblabber please post your code. because my screen shot is the same as @Olx – Saad Masood Dec 13 '13 at 13:33
  • The server reponds with the following code in RoR. ==> `render status:400, json:{message:"Please enter the user email and password."} return` – Saad Masood Dec 13 '13 at 14:02
  • what sort of response does the API need to send in order for the `userInfo` property to get populated? – wprater May 14 '14 at 23:53