Centralized error handling for AFNetworking - ios

I'm looking for a way to handle "generic" errors such as request timeouts or for when the connection goes offline.
Basically, I have multiple (singleton) subclasses of AFHTTPSessionManager where each one represents a client that handles requests to different servers. Each client is setup by overriding initWithBaseURL as recommended by the author of AFNetworking; this is where the request/response serializers as well as generic headers are set. Here's a sample client:
#implementation APIClient
+ (APIClient *)sharedClient {
static APIClient *sharedClient = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedClient = [[self alloc] initWithBaseURL:[NSURL URLWithString:#"baseurl.goes.here"]];
});
return sharedClient;
}
- (instancetype)initWithBaseURL:(NSURL *)url
{
self = [super initWithBaseURL:url];
if(self) {
// Setup goes here
self.requestSerializer = [AFHTTPRequestSerializer serializer];
self.requestSerializer.timeoutInterval = 20.0f;
self.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:#"text/plain", #"text/html", nil];
[AFNetworkActivityIndicatorManager sharedManager].enabled = YES;
[AFNetworkActivityLogger sharedLogger].level = AFLoggerLevelDebug;
[[AFNetworkActivityLogger sharedLogger] startLogging];
}
return self;
}
- (void)startPostRequestWithPath:(NSString *)path parameters:(NSDictionary *)parameters successBlock:(APISuccessBlock)success failureBlock:(APIFailureBlock)failure
{
[self POST:path parameters:parameters
success:^(NSURLSessionDataTask * _Nonnull task, id _Nonnull responseObject) {
success(responseObject);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
if(isGenericError) {
// Do something generic here
}
else {
failure(error);
}
}];
}
Inside my model (e.g, Post), I have a static method that can be used by the view controller to fetch the data by passing its own success/failure blocks to the client. So it goes like this:
View Controller --> Model --> Client --> Model --> View Controller.
And here's the implementation of the model
#implementation Post
+ (void)fetchLatestPost:(void (^)(Post *parsedData, NSError *error))completion
{
[[APIClient sharedClient] startRequestWithPath:kIndexPath
parameters:nil
requestType:RequestTypePost
successBlock:^(id data) {
NSError *parsingError = nil;
Post *post = [[Index alloc] initWithDictionary:data error:&err];
completion(index, nil);
}
failureBlock:^(NSError *error) {
completion(nil, error);
}
];
}
When a view controller tries to fetch that Post and the request times out, I'd like to hide the contents of the screen and show a refresh button; this logic is implemented in my BaseViewController so that all view controllers can reuse it. The question is, how do I restart the SAME request when that button is clicked? Do note that a view controller can make multiple requests from different models with different method signatures. Any help would be greatly appreciated as I can't seem to figure this out at all.
I used to handle this using delegates, where the BaseViewController would implement the "generic" delegate methods. However, I've been trying to switch to blocks and while it does have its advantages, it doesn't allow me to make use of my BaseViewController since it's can't "override" the view controller's failure blocks.

Related

Get specific instance of class from inside super class class method

+ (NSURLSessionDataTask *)login:(NSString*)email andPassword:(NSString*)password andCallback:(void (^)(NSArray *responseArray, NSError *error))block {
if(![self hasInternet]){return nil;}
NSLog(#"Session.login");
[APIClient sharedClient].requestSerializer = [AFJSONRequestSerializer serializer];
[[APIClient sharedClient].requestSerializer setValue:email forHTTPHeaderField:#"email"];
[[APIClient sharedClient].requestSerializer setValue:password forHTTPHeaderField:#"password"];
[[APIClient sharedClient].requestSerializer setValue:#"poop" forHTTPHeaderField:#"apikey"];
return [[APIClient sharedClient] POST:#"/login" parameters:nil progress:nil success:^(NSURLSessionDataTask * __unused task, id JSON) {
NSLog(#"session.loginWithEmail.response:%#",JSON);
if([JSON objectForKey:#"user"]){
NSMutableDictionary *user=[[NSMutableDictionary alloc] initWithDictionary:[[JSON objectForKey:#"user"] copy]];
[user setObject:password forKey:#"password"];
[[Session sharedInstance] startSession:user];
if([[Session sharedInstance] isSessionActive]){
if([JSON objectForKey:#"req_onboarding"]){
NSLog(#"session.onboard!=nil");
[Session sharedInstance].requiredOnboarding=[JSON objectForKey:#"req_onboarding"];
}
if (block) {
NSLog(#"session.login.block.success");
block(nil, nil);
}
}else{
NSLog(#"Failed to set session");
}
}
} failure:^(NSURLSessionDataTask *__unused task, NSError *error) {
if (block) {
NSLog(#"Session.login.Fail");
block([NSArray array], error);
}
}];
}
I needed a sub-class-able singleton in order to be able to have a abstracted session manager that does most of the lifting,but can still be subclassed so that multiple sessions can co-exist and still have the power of being available throughout my app. Im building somewhat of a demo of all my apps which is why this functionality is important.
All was going well until I realized that my api methods that are hosted in the super session class were referencing the singleton itself to set the session, this is a problem bc sharedInstance is referenced like so:
+ (instancetype)sharedInstance
{
NSLog(#"[Master sharedInstance]");
id sharedInstance = nil;
#synchronized(self) {
NSLog(#"MS | synchronized(self)");
NSString *instanceClass = NSStringFromClass(self);
// Looking for existing instance
sharedInstance = [_sharedInstances objectForKey:instanceClass];
// If there's no instance – create one and add it to the dictionary
if (sharedInstance == nil) {
NSLog(#"MS | sharedInstance == nil");
sharedInstance = [[super allocWithZone:nil] init];
[_sharedInstances setObject:sharedInstance forKey:instanceClass];
NSLog(#"MS | SharedInstances:%#",_sharedInstances);
}
}
return sharedInstance;
}
When it was just the one Session singleton I could get away with doing this in class methods: [Session sharedInstance] isSessionActive]
but now, its essential that [______ sharedInstance] isSessionActive];
is a reference to the specific subclass calling the class method. Is it possible to retrieve reference the specific instance from within this class method shy of sending it as a param?
It looks like the aim is to distribute a singleton per subclass of the super (probably abstract) class. The sharedInstance code doesn't quite do that because this line:
sharedInstance = [[super allocWithZone:nil] init];
will create instances only of the superclass. I think you want instances of the subclasses so that you get access to the overridden data and behavior.
If I understand your aim correctly, then the fix is simple:
sharedInstance = [[self allocWithZone:nil] init]; // notice "self"
With this, when you send sharedInstance to ClassA, you'll get a (single) instance of ClassA. When you send it to ClassB, you'll get a (single) instance of ClassB. With change I suggest, those will really be instances of the subclasses A and B, not an instance of the class they both inherit from. If they've each overridden isSessionActive or any other superclass method, the caller will get the distinct, overridden implementation based on which class singleton they ask for.

AFHTTPSessionManager get error response from server [duplicate]

I've been using AFNetworking 2.0 in my app.
I've noticed that if my web-service returns a 500 status code I do not get the body of the response.
Here is an example of my php code
try
{
$conn = new PDO( "sqlsrv:server=$serverName;Database = $database", $uid, $pwd);
$conn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
return $conn;
}
catch( PDOException $e )
{
$response->status(500);
echo( "Connection Error: " . $e->getMessage() );
}
If I use a simple rest client this is an example of a response body.
Connection Error: SQLSTATE[08001]: [Microsoft][SQL Server Native Client 11.0]SQL Server Network Interfaces: Error Locating Server/Instance Specified [xFFFFFFFF].
However this seems to be the only response I can get from AFNetworking
Error Domain=NSCocoaErrorDomain Code=3840 "The operation couldn’t be completed. (Cocoa error 3840.)" (JSON text did not start with array or object and option to allow fragments not set.) UserInfo=0x15e58fa0 {NSDebugDescription=JSON text did not start with array or object and option to allow fragments not set.}
This is the part of my objective-c code that does this.
...} failure:^(NSURLSessionDataTask *task, NSError *error) {
NSLog(#"%#",error.description);
}];
Is there a way I can get the response body?
Edit: More code for clarification
Below is part of my subclass of AFHTTPSessionManager
#implementation MSMAMobileAPIClient
+ (MSMAMobileAPIClient *)sharedClient {
static MSMAMobileAPIClient *_sharedClient = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedClient = [[MSMAMobileAPIClient alloc] initWithDefaultURL];
});
return _sharedClient;
}
- (id)initWithDefaultURL {
return [self initWithBaseURL:[NSURL URLWithString:[NSString stringWithFormat:#"https://%#/mamobile/index.php/" ,[[NSUserDefaults standardUserDefaults] stringForKey:#"serviceIPAddress"]]]];
}
- (id)initWithBaseURL:(NSURL *)url {
self = [super initWithBaseURL:url];
if (!self) {
return nil;
}
self.responseSerializer = [AFCompoundResponseSerializer compoundSerializerWithResponseSerializers:#[[AFJSONResponseSerializer serializer], [AFHTTPResponseSerializer serializer]]];
return self;
}
I tried setting the response serializer to a AFCompoundResponseSerializer but it didn't seem to make a difference
Below is an example of a subclass that I call the Librarian.
-(void)searchForItemWithString:(NSString *)searchString withCompletionBlock:(arrayBlock)block {
self.inventorySearchBlock = block;
NSDictionary *parameters = #{#"query": searchString};
[[MSMAMobileAPIClient sharedClient] GET:#"inventory/search" parameters:parameters success:^(NSURLSessionDataTask *task, id responseObject) {
if (!responseObject) {
NSLog(#"Error parsing JSON");
} else {
//do stuff with the json dictionary that's returned..
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
NSLog(#"Error: %#",error.description);
}];
}
UPDATE: I have created a github repository to contain the latest code I am using. All changes will be posted there. https://github.com/Hackmodford/HMFJSONResponseSerializerWithData
The answer comes from this issue on github.
https://github.com/AFNetworking/AFNetworking/issues/1397
gfiumara is the dev who came up with this. I have only slightly modified his subclass of AFJSONResponseSerializer to include an actual string instead of the NSData
//MSJSONResponseSerializerWithData.h
#import "AFURLResponseSerialization.h"
/// NSError userInfo key that will contain response data
static NSString * const JSONResponseSerializerWithDataKey = #"JSONResponseSerializerWithDataKey";
#interface MSJSONResponseSerializerWithData : AFJSONResponseSerializer
#end
// MSJSONResponseSerializerWithData.m
#import "MSJSONResponseSerializerWithData.h"
#implementation MSJSONResponseSerializerWithData
- (id)responseObjectForResponse:(NSURLResponse *)response
data:(NSData *)data
error:(NSError *__autoreleasing *)error
{
if (![self validateResponse:(NSHTTPURLResponse *)response data:data error:error]) {
if (*error != nil) {
NSMutableDictionary *userInfo = [(*error).userInfo mutableCopy];
userInfo[JSONResponseSerializerWithDataKey] = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSError *newError = [NSError errorWithDomain:(*error).domain code:(*error).code userInfo:userInfo];
(*error) = newError;
}
return (nil);
}
return ([super responseObjectForResponse:response data:data error:error]);
}
#end
Here is an example of how I use it in the failure block.
} failure:^(NSURLSessionDataTask *task, NSError *error) {
NSLog(#"%#",[error.userInfo objectForKey:#"JSONResponseSerializerWithDataKey"]);
}];
You need to use AFCompoundSerializer to tell the AFNetworking framework how to process all of the possible responses it could receive. By default it will only try to map JSON. A compound serializer will work through the serializers until it finds one that doesn't raise an error.
You want to use:
+ (instancetype)compoundSerializerWithResponseSerializers:(NSArray *)responseSerializers
on AFCompoundResponseSerializer (in AFURLResponseSerialization.h).
You need to pass an array of serializers that can handle the response. One of the serializers in the array should be an instance of AFHTTPResponseSerializer to handle your error responses.
If you include my category in your project, it's as simple as the following:
[mySessionManager POST:#"some-api" parameters:params success:^(NSURLSessionDataTask *task, NSDictionary *responseObject) {
...
} failure:^(NSURLSessionDataTask *task, NSError *error) {
id responseObject = error.userInfo[kErrorResponseObjectKey];
... do something with the response ...
}];
Here's the code for my category. It swizzles AFURLSessionManager to inject a shim into the completion handler. The shim puts the response into the NSError's userInfo.
https://gist.github.com/chrishulbert/35ecbec4b37d36b0d608

GET request using AFNetworking and saving response

I am doing a simple GET request with AFNetworking
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager GET:#"http://someapi.com/hello.json" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"JSON: %#", responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
Once I have made the request I want to be able to access the responseObject from any other method in the class.
I want to be able to save the responseObject so I can do something like display the output in a tableview.
It's common to creat object models that will be represented by JSON. When you get the response you would then parse the data into the models. The approach we use is to return the response to the requester through a completion block. You don't have to parse the JSON into strongly typed objects, but it really is helpful long term. It's probably a good idea to farm out the network request operations into a separate class (called a service) as well. This way you can instantiate a new service and get notified through a completion block that it is finished. For example your service's request signature could look like this:
typedef void(^HelloWorldCompletionHandler)(NSString *helloWorld, NSError *error);
- (void)requestHelloWorldData:(HelloWorldCompletionHandler)completionHandler;
// implementation
- (void)requestHelloWorldData:(HelloWorldCompletionHandler)completionHandler {
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager GET:#"http://someapi.com/hello.json" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
id JSONResponse = [operation responseObject];
if (operation.error) {
completionHandler(nil, error);
} else {
// parse the response to something
id parserResult = [self parseJSONResponse:JSONResponse];
completionHandler(parserResult, nil);
}
}];
This way you'll know when the network request is complete, and you can set the data you want on a property within your class. Then you could call tableView.reloadData in order to use the data in your table.
All that code would go into a service type class. I like to organize my services by responsibility. I don't know how many different data calls you make, but we have several for our project. If for instance you were making a weather app you could potentially organize by Current Conditions, Daily Forecasts, and Hourly Forecasts. I would make a service for each one of these requests. Say I created a CurrentConditionsService. The header would look something like this:
typedef void(^CurrentConditionsCompletionHandler)(CurrentConditions *currentConditions, NSError *error);
#interface CurrentConditionsService : NSObject
// locationKey is some unique identifier for a city
+ (instancetype)serviceWithLocationKey:(NSString *)locationKey;
- (void)retrieveCurrentConditionsWithCompletionHandler:(CurrentConditionsCompletionHandler)completionHandler;
#end
Then in my implementation file I would make the request and invoke the given completion handler like I demonstrated above. This pattern can be followed by many different services to the point where all your services could inherit from a base class that handles the request/response portions. Then your subclasses could override specific methods and handle/parse the data appropriately based on type.
If you go the route of parsing the JSON responses into model objects, all your parsers will need to conform to a protocol. This way in your super class it doesn't matter what the concrete implementation of your parser is. You supply the super class with a concrete implementation and all it knows how to do is invoke the parser and return the response.
An example JSON parser protocol would look like this:
#protocol AWDataParser <NSObject>
#required
- (id)parseFromDictionary:(NSDictionary *)dictionary;
- (NSArray *)parseFromArray:(NSArray *)array;
#end
And invoking it in your services super class:
- (id)parseJSONResponse:(id)JSONResponse error:(NSError **)error {
NSAssert(self.expectedJSONResponseClass != nil, #"parseResponse: expectedJSONResponseClass cannot be nil");
NSAssert(self.parser != nil, #"parseResponse: parser cannot be nil");
id parserResult = nil;
if (![JSONResponse isKindOfClass:self.expectedJSONResponseClass]) {
//handle invalid JSON reponse object
if (error) {
*error = [NSError errorWithDomain:NetworkServiceErrorDomain code:kNetworkServiceErrorParsingFailure userInfo:#{#"Invalid JSON type": [NSString stringWithFormat:#"expected: %#, is: %#",self.expectedJSONResponseClass, [JSONResponse class]]}];
}
} else {
if (self.expectedJSONResponseClass == [NSArray class]) {
parserResult = [self.parser parseFromArray:JSONResponse];
}else {
parserResult = [self.parser parseFromDictionary:JSONResponse];
}
if (!parserResult) {
if (error) {
*error = [NSError errorWithDomain:NetworkServiceErrorDomain code:kNetworkServiceErrorParsingFailure userInfo:nil];
}
}
}
return parserResult;
}
Use this approach:
NSURL *COMBINAT = [[NSURL alloc] initWithString:#"http://someapi.com/hello.json"];
dispatch_async(kBgQueue, ^{
NSData* data = [NSData dataWithContentsOfURL:
COMBINAT];
[self performSelectorOnMainThread:#selector(savedata:) withObject:data waitUntilDone:YES];
});
then simply call:
- (void)savedata:(NSData *)responseData {
NSError* error;
NSLog(#"Answer from server %#", responseData);
  // ... your code to use responseData
}
Just create a property:
#property(nonatomic, strong) id savedResponseObject;
and set it in the success handler of the request:
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager GET:#"http://someapi.com/hello.json"
parameters:nil
success:^(AFHTTPRequestOperation *operation, id responseObject)
{
self.savedResponseObject = responseObject;
}
failure:^(AFHTTPRequestOperation *operation, NSError *error)
{
NSLog(#"Error: %#", error);
}];
Then you will be able to access it from other places in your class by referencing:
self.savedResponseObject

AFHTTPSessionManager with multiple requests in rapid succession (AFNetworking 2.0)

i am new to iOS programming, still learning.
EDIT: !!!!!! Everything in my code works. My question is about the delegation pattern i use,
if i am generating problems in the background that i have no idea of, or if there is a better way to handle my situation in AFNetworking...
I have created an API for my app by subclassing AFHTTPSessionManager.
My API creates a singleton and returns it and supplies public functions for various requests. And those functions create parameter lists, and make GET requests on the server like this:
- (void)getCharacterListForKeyID:(NSString *)keyID vCode:(NSString *)vCode sender:(id)delegate
{
NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
parameters[#"keyID"] = keyID;
parameters[#"vCode"] = vCode;
[self GET:#"account/Characters.xml.aspx" parameters:parameters success:^(NSURLSessionDataTask *task, id responseObject) {
self.xmlWholeData = [NSMutableDictionary dictionary];
self.errorDictionary = [NSMutableDictionary dictionary];
NSXMLParser *XMLParser = (NSXMLParser *)responseObject;
[XMLParser setShouldProcessNamespaces:YES];
XMLParser.delegate = self;
[XMLParser parse];
if ([delegate respondsToSelector:#selector(EVEAPIHTTPClient:didHTTPRequestWithResult:)]) {
[delegate EVEAPIHTTPClient:self didHTTPRequestWithResult:self.xmlWholeData];
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
if ([delegate respondsToSelector:#selector(EVEAPIHTTPClient:didFailWithError:)]) {
[delegate EVEAPIHTTPClient:self didFailWithError:error];
}
}];
}
I was using a normal protocol/delegate method earlier. But once i make calls this API more than once like this: (IT WAS LIKE THIS:)
EVEAPIHTTPClient *client = [EVEAPIHTTPClient sharedEVEAPIHTTPClient];
client.delegate = self;
[client getCharacterListForKeyID:self.keyID vCode:self.vCode];
Previous call's delegate was being overwritten by next. So i changed to above style. Passing sender as an argument in the function:
EVEAPIHTTPClient *client = [EVEAPIHTTPClient sharedEVEAPIHTTPClient];
[client getCharacterListForKeyID:self.keyID vCode:self.vCode sender:self];
And i pass this sender to GET request's success and failure blocks.
What i wonder is : "Is this a good programming practice ?". Passing objects to blocks like this should be avoided if possible ? Is there any other more elegant way in AFHTTPSessionManager to handle this type of work (making same GET request over and over with different parameters and returning results to the respective request owners) more elegantly ?
Delegation pattern falters when it comes to simplicity and asynchronous request processing. You should be using blocks, here's an example
Your server class:
static NSString *const kNews = #"user_news/"; // somewhere above the #implementation
- (NSURLSessionDataTask *)newsWithPage:(NSNumber *)page
lastNewsID:(NSNumber *)lastNewsID
completion:(void (^)(NSString *errMsg, NSArray *news, NSNumber *nextPage))completionBlock {
return [self GET:kNews
parameters:#{#"page" : page,
#"news_id" : lastNewsID
}
success:^(NSURLSessionDataTask *task, id responseObject) {
NSArray *news = nil;
NSNumber *nextPage = nil;
NSString *errors = [self errors:responseObject[#"errors"]]; // process errors
if ([responseObject[#"status"] boolValue]) {
news = responseObject[#"news"];
nextPage = responseObject[#"next_page"];
[self assignToken];
}
completionBlock(errors, news, nextPage);
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
NSString *errors = [self errors:error];
completionBlock(errors, nil, nil);
}];
}
The caller
- (void)dealloc {
[_task cancel]; // you don't want this task to execute if user suddenly removes your controller from the navigation controller's stack
}
- (void)requestNews {
typeof(self) __weak wself = self; // to avoid the retain cycle
self.task = [[GSGServer sharedInstance] newsWithPage:self.page
lastNewsID:self.lastNewsID
completion:^(NSString *errMsg, NSArray *news, NSNumber *nextPage) {
if (errMsg) {
[GSGAppDelegate alertQuick:errMsg]; // shortcut for posting UIAlertView, uses errMsg for message and "Error" as a title
return;
}
[wself.news addObjectsFromArray:news];
wself.lastNewsID = [wself.news firstObject][#"id"];
wself.page = nextPage;
[wself.tableView reloadData];
}];
}

AFNetworking 500 response body

I've been using AFNetworking 2.0 in my app.
I've noticed that if my web-service returns a 500 status code I do not get the body of the response.
Here is an example of my php code
try
{
$conn = new PDO( "sqlsrv:server=$serverName;Database = $database", $uid, $pwd);
$conn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
return $conn;
}
catch( PDOException $e )
{
$response->status(500);
echo( "Connection Error: " . $e->getMessage() );
}
If I use a simple rest client this is an example of a response body.
Connection Error: SQLSTATE[08001]: [Microsoft][SQL Server Native Client 11.0]SQL Server Network Interfaces: Error Locating Server/Instance Specified [xFFFFFFFF].
However this seems to be the only response I can get from AFNetworking
Error Domain=NSCocoaErrorDomain Code=3840 "The operation couldn’t be completed. (Cocoa error 3840.)" (JSON text did not start with array or object and option to allow fragments not set.) UserInfo=0x15e58fa0 {NSDebugDescription=JSON text did not start with array or object and option to allow fragments not set.}
This is the part of my objective-c code that does this.
...} failure:^(NSURLSessionDataTask *task, NSError *error) {
NSLog(#"%#",error.description);
}];
Is there a way I can get the response body?
Edit: More code for clarification
Below is part of my subclass of AFHTTPSessionManager
#implementation MSMAMobileAPIClient
+ (MSMAMobileAPIClient *)sharedClient {
static MSMAMobileAPIClient *_sharedClient = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedClient = [[MSMAMobileAPIClient alloc] initWithDefaultURL];
});
return _sharedClient;
}
- (id)initWithDefaultURL {
return [self initWithBaseURL:[NSURL URLWithString:[NSString stringWithFormat:#"https://%#/mamobile/index.php/" ,[[NSUserDefaults standardUserDefaults] stringForKey:#"serviceIPAddress"]]]];
}
- (id)initWithBaseURL:(NSURL *)url {
self = [super initWithBaseURL:url];
if (!self) {
return nil;
}
self.responseSerializer = [AFCompoundResponseSerializer compoundSerializerWithResponseSerializers:#[[AFJSONResponseSerializer serializer], [AFHTTPResponseSerializer serializer]]];
return self;
}
I tried setting the response serializer to a AFCompoundResponseSerializer but it didn't seem to make a difference
Below is an example of a subclass that I call the Librarian.
-(void)searchForItemWithString:(NSString *)searchString withCompletionBlock:(arrayBlock)block {
self.inventorySearchBlock = block;
NSDictionary *parameters = #{#"query": searchString};
[[MSMAMobileAPIClient sharedClient] GET:#"inventory/search" parameters:parameters success:^(NSURLSessionDataTask *task, id responseObject) {
if (!responseObject) {
NSLog(#"Error parsing JSON");
} else {
//do stuff with the json dictionary that's returned..
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
NSLog(#"Error: %#",error.description);
}];
}
UPDATE: I have created a github repository to contain the latest code I am using. All changes will be posted there. https://github.com/Hackmodford/HMFJSONResponseSerializerWithData
The answer comes from this issue on github.
https://github.com/AFNetworking/AFNetworking/issues/1397
gfiumara is the dev who came up with this. I have only slightly modified his subclass of AFJSONResponseSerializer to include an actual string instead of the NSData
//MSJSONResponseSerializerWithData.h
#import "AFURLResponseSerialization.h"
/// NSError userInfo key that will contain response data
static NSString * const JSONResponseSerializerWithDataKey = #"JSONResponseSerializerWithDataKey";
#interface MSJSONResponseSerializerWithData : AFJSONResponseSerializer
#end
// MSJSONResponseSerializerWithData.m
#import "MSJSONResponseSerializerWithData.h"
#implementation MSJSONResponseSerializerWithData
- (id)responseObjectForResponse:(NSURLResponse *)response
data:(NSData *)data
error:(NSError *__autoreleasing *)error
{
if (![self validateResponse:(NSHTTPURLResponse *)response data:data error:error]) {
if (*error != nil) {
NSMutableDictionary *userInfo = [(*error).userInfo mutableCopy];
userInfo[JSONResponseSerializerWithDataKey] = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSError *newError = [NSError errorWithDomain:(*error).domain code:(*error).code userInfo:userInfo];
(*error) = newError;
}
return (nil);
}
return ([super responseObjectForResponse:response data:data error:error]);
}
#end
Here is an example of how I use it in the failure block.
} failure:^(NSURLSessionDataTask *task, NSError *error) {
NSLog(#"%#",[error.userInfo objectForKey:#"JSONResponseSerializerWithDataKey"]);
}];
You need to use AFCompoundSerializer to tell the AFNetworking framework how to process all of the possible responses it could receive. By default it will only try to map JSON. A compound serializer will work through the serializers until it finds one that doesn't raise an error.
You want to use:
+ (instancetype)compoundSerializerWithResponseSerializers:(NSArray *)responseSerializers
on AFCompoundResponseSerializer (in AFURLResponseSerialization.h).
You need to pass an array of serializers that can handle the response. One of the serializers in the array should be an instance of AFHTTPResponseSerializer to handle your error responses.
If you include my category in your project, it's as simple as the following:
[mySessionManager POST:#"some-api" parameters:params success:^(NSURLSessionDataTask *task, NSDictionary *responseObject) {
...
} failure:^(NSURLSessionDataTask *task, NSError *error) {
id responseObject = error.userInfo[kErrorResponseObjectKey];
... do something with the response ...
}];
Here's the code for my category. It swizzles AFURLSessionManager to inject a shim into the completion handler. The shim puts the response into the NSError's userInfo.
https://gist.github.com/chrishulbert/35ecbec4b37d36b0d608

Resources