Using Restkit, I've setup the RKObjectManager in my AppDelegate and everything is working fine. I would like to know if there's some way to setup a default action for specific response codes.
For example, a user uses my iPhone app to login to my api and gets an auth_token back to use. If at any point, for any request, I get back I get a 403 response (like if the auth_token expires) I want to change the RootViewController to my login screen.
What would be the best way to set this up in my app?
In RestKit 0.20 you can register your RKObjectRequestOperation, so you can pass all requests/responses through your own success/failure blocks before anything else.
http://blog.higgsboson.tk/2013/09/03/global-request-management-with-restkit/
#import "RKObjectRequestOperation.h"
#interface CustomRKObjectRequestOperation : RKObjectRequestOperation
#end
#implementation CustomRKObjectRequestOperation
- (void)setCompletionBlockWithSuccess:(void ( ^ ) ( RKObjectRequestOperation *operation , RKMappingResult *mappingResult ))success failure:(void ( ^ ) ( RKObjectRequestOperation *operation , NSError *error ))failure
{
[super setCompletionBlockWithSuccess:^void(RKObjectRequestOperation *operation , RKMappingResult *mappingResult) {
if (success) {
success(operation, mappingResult);
}
}failure:^void(RKObjectRequestOperation *operation , NSError *error) {
[[NSNotificationCenter defaultCenter] postNotificationName:#"connectionFailure" object:operation];
if (failure) {
failure(operation, error);
}
}];
}
#end
Then register your subclass:
[[RKObjectManager sharedManager] registerRequestOperationClass:[CustomRKObjectRequestOperation class]];
And listen for the "connectionFailure" you are sending above:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(connectionFailedWithOperation:) name:#"connectionFailure" object:nil];
In the listener (e.g. your AppDelegate or a login manager):
- (void)connectionFailedWithOperation:(NSNotification *)notification
{
RKObjectRequestOperation *operation = (RKObjectRequestOperation *)notification.object;
if (operation) {
NSInteger statusCode = operation.HTTPRequestOperation.response.statusCode;
switch (statusCode) {
case 0: // No internet connection
{
}
break;
case 401: // not authenticated
{
}
break;
default:
{
}
break;
}
}
}
When using RestKit 0.10 you can use the given delegate method objectLoaderDidLoadUnexpectedResponse.
- (void)objectLoaderDidLoadUnexpectedResponse:(RKObjectLoader *)objectLoader {
if ([[objectLoader response] statusCode] == 403) {
// Your action here
}
}
In RestKit 0.20 you can use a response descriptor for a single code or a set of codes.
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:responseMapping
pathPattern:nil
keyPath:#"yourKeyPath"
statusCodes:[NSIndexSet indexSetWithIndex:403]];
More status code sets in the documentation.
Update
When using your BaseViewController for handling the errors of the request made in one of the other view controllers, you can set up notifications.
BaseViewController
- (void)viewDidLoad
{
// ...
// Set observer for notification e.g. "requestFailedWith403Error"
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(handle403Error:) name:#"requestFailedWith403Error" object:self];
}
- (void)handle403Error:(NSNotification)notification
{
// Code for handling the error
}
SubViewController
- (void)loginToServer
{
// ...
// Set authorization header
[[RKObjectManager sharedManager].HTTPClient setAuthorizationHeaderWithUsername:#"username" password:#"password"];
// e.g. POST to server
[[RKObjectManager sharedManager] postObject:yourObject
path:#"path/toserver"
parameters:nil
success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
// Handling success
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
// Handling error with notification
[[NSNotificationCenter defaultCenter] postNotificationName:#"requestFailedWith403Error" object:self];
}];
}
Too optimize your central configuration with the handling of errors you can have another look at the example code given in the RestKit Wiki (where the error mapping is added).
Related
Is there a way to differ between mapping results which are empty due to there being no data do map att the key path and a mapping result which is empty due do skipping objects through KVC.
The problem I have is that I am working with a API which is not very well thought through, and I need to find a way to know when I am at the end of the page. The issue is that there is a possibility that the mapping result can be empty due to all the objects being returned are "deleted".
TLDR; Is there a way to see the amount of mapping errors through mapping result on a completed request.
EDIT
I solved my own problem by reading the documentation again. The solution was to create a subclass of RKObjectRequestOperation and then register it.
[[RKObjectManager sharedManager] registerRequestOperationClass:[CustomRequestOperation class]];
This mean that I could implement the following which would listen to mapping errors and increment a counter and add it to mappingMetaData when the mapping completed.
- (void)setCompletionBlockWithSuccess:(void ( ^ ) ( RKObjectRequestOperation *operation , RKMappingResult *mappingResult ))success failure:(void (^) ( RKObjectRequestOperation *operation , NSError *error ))failure {
__block typeof(skipCount) temp = skipCount;
[super setCompletionBlockWithSuccess:^void(RKObjectRequestOperation *operation , RKMappingResult *mappingResult) {
if (success) {
operation.mappingMetadata = #{#"skipCount" : #(temp)};
success(operation, mappingResult);
}
} failure:^void(RKObjectRequestOperation *operation , NSError *error) {
if (failure) {
failure(operation, error);
}
}];
}
- (void)mapper:(RKMapperOperation *)mapper didFailMappingOperation:(RKMappingOperation *)mappingOperation forKeyPath:(NSString *)keyPath withError:(NSError *)error {
skipCount++;
}
- (void)mapperWillStartMapping:(RKMapperOperation *)mapper {
skipCount = 0;
}
Is they a way to add some url parameters (like http://api.example.com/v3/object?data=123&info=test) to all restkit request witouth adding them manually to all
getObjectsAtPath:parameters:success:failure:
getObjectsAtPathForRouteNamed:object:parameters:success:failure:
...
each request should add the info parameter.
I've actually a way to do it, using Method Swizzling. Is they a way to do it directly with RestKit?
You have a couple of ways to do this:
you can either subclass the methods of RKObjectManager to something like this:
-(void)addedParamToGetObjectsAtPath:(NSString*)path parameters:(NSDictionary*)parameters success:(success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult)successBlock failure::^(RKObjectRequestOperation *operation, NSError *error){
NSMutableDictionary* newParams = [NSMutableDictionary new];
if(parameters){
[newParams addEntriesFromDictionary:parameters];
}
newParams[#"info"]=test;
getObjectsAtPath:(NSString*)path parameters:(NSDictionary*)parameters success:(success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult){
// Deal with the success here
successBlock(operation, mappingResult);
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
//Deal with the error here
errorBlock(operation, error);
}];
Or tell Restkit to use a different RequestOperationClass
//When configuring RestKit
RKObjectManager *objectManager = [RKObjectManager managerWithBaseURL:[NSURL URLWithString:BASE_URL]];
//Some more configuration
//....
[objectManager registerRequestOperationClass:[YourObjectRequestOperation class]];
And define a subclass of RKObjectRequestOperation, YourObjectRequestOperation
#import "FBObjectRequestOperation.h"
#interface RKHTTPRequestOperation ()
#property (nonatomic, strong, readwrite) NSMutableURLRequest* request;
#end
#implementation FBObjectRequestOperation
- (id)initWithHTTPRequestOperation:(RKHTTPRequestOperation *)requestOperation responseDescriptors:(NSArray *)responseDescriptors
{
NSParameterAssert(requestOperation);
NSParameterAssert(responseDescriptors);
//your method to change the requestOperation
RKHTTPRequestOperation* myRequestOperation = [YourObjectRequestOperation addParametersToRequest:requestOperation];
self = [super initWithHTTPRequestOperation:myRequestOperation responseDescriptors:responseDescriptors];
if (self) {
//Change headers or any other thing that you need
}
return self;
}
To actually change the requestOperation you will need to get the url from the request and and add there the new parameters. That will happen in this part RKHTTPRequestOperation* myRequestOperation = [YourObjectRequestOperation addParametersToRequest:requestOperation]; and I am living up to you to complete the code.
This should work for any request you are doing with the object manager.
This technique is also very helpful is you need to calculate headers dynamically for each request.
I am trying to figure out how long my round trip to the server is before any mapping occurs. I need to get at the RKObjectRequestOperation, but it is only available in the success and fail blocks.
I see that RestKit 2 does send a notification:
[[NSNotificationCenter defaultCenter] postNotificationName:RKObjectRequestOperationDidStartNotification object:weakSelf];
But there is no user info sent along.
Any ideas on how I can do this? I was thinking of an associated object onto the operation queue but that is causing crashes.
What I did:
self.op = self.objectManager.operationQueue.operations.lastObject;
objc_setAssociatedObject(self, &kRetrieverRequestOperationKey, self.op, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
Added the above right after:
[self.objectManager getObjectsAtPath:resourcePath parameters:parmsDictionary success:^(RKObjectRequestOperation *requestOperation, RKMappingResult *mappingResult)
{
[weakSelf didLoadOperation:requestOperation result:mappingResult isFromCache:NO];
[weakSelf requestDidEnd:requestOperation];
} failure:^(RKObjectRequestOperation *requestOperation, NSError *error) {
[weakSelf requestOperation:requestOperation didFailWithError:error];
[weakSelf requestDidEnd:requestOperation];
}];
Then when RestKit posted its notification I was able to get at the RKObjectRequestOperation.
Not ideal, but seems to work.
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
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