When logging a user into my application I need to pull a user object down from the server using only the username. This returns the userId (among other things) that I need in order to make other API calls. From that point I'll make a couple other HTTP calls using the userId. How can I make a synchronous call to completely pull down the user object before sending the other calls?
I've setup my object mapping in my app delegate class, which works perfectly, and am using this code to pull the user object down from the server:
[[RKObjectManager sharedManager] loadObjectsAtResourcePath:[#"/api/users/" stringByAppendingString:[_userNameField text]] delegate:self];
This is what I've tried... as suggested here: Making synchronous calls with RestKit
RKObjectLoader* loader = [[RKObjectManager sharedManager] objectLoaderForObject:currentUser method:RKRequestMethodPUT delegate:nil];
RKResponse* response = [loader sendSynchronously];
However this code (1) uses the deprecated method objectLoaderForObject and (2) crashes saying 'Unable to find a routable path for object of type '(null)' for HTTP Method 'POST''.
Putting aside the question of whether this is the ideal design for an iPhone application, I was able to accomplish what I was hoping using blocks.
[[RKObjectManager sharedManager] loadObjectsAtResourcePath:[#"/api/users/" stringByAppendingString:[_userNameField text]] usingBlock:^(RKObjectLoader* loader) {
loader.onDidLoadResponse = ^(RKResponse *response) {
NSLog(#"Response: \n%#", [response bodyAsString]);
};
loader.onDidLoadObjects = ^(NSArray *objects) {
APIUser *apiUser = [objects objectAtIndex:0];
NSLog(#"user_id is %i", apiUser.user_id);
};
loader.onDidFailWithError = ^(NSError *error) {
UIAlertView *badLoginAlert = [[UIAlertView alloc]initWithTitle:NSLocalizedString(#"LOGIN_FAILED", nil)
message:NSLocalizedString(#"BAD PASSWORD OR USERNAME", nil)
delegate:self
cancelButtonTitle:NSLocalizedString(#"OK", nil)
otherButtonTitles:nil];
[badLoginAlert show];
};
}];
Hope this helps someone.
Related
I am having an issue parsing two JSON urls at once. YouTube only permits 50 results per request, so I'd like to add a second with a start-index of 51, to continue the request.
NSString *urlAsString = #"https://gdata.youtube.com/feeds/api/playlists/PLgw1uRYia2CRvuF4Y3KLuvFSWY6lmuY8T?v=2&alt=json&max-results=50&orderby=published";
NSString *urlAsString2 = #"https://gdata.youtube.com/feeds/api/playlists/PLgw1uRYia2CTSBBNrTDjdEcswVFjPkCr9?v=2&alt=json&max-results=50&orderby=published";
Combining two of them, I tried this:
NSString *finallink = [NSString stringWithFormat:#"%#,%#", urlAsString, urlAsString2];
Then making the actual request with Afnetworking, I added:
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager GET:finallink parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
manager.requestSerializer = [AFHTTPRequestSerializer serializer];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
NSDictionary *feed = [[NSDictionary alloc] initWithDictionary:[responseObject valueForKey:#"feed"]];
videoArray = [NSMutableArray arrayWithArray:[feed valueForKey:#"entry"]];
[self.videoMetaData addObjectsFromArray:[videoArray valueForKeyPath:#"title.$t"]];
operation.responseSerializer.acceptableContentTypes = [NSSet setWithObject:#"text/html"];
[self.videolist reloadData];
[self->activityind startAnimating];
NSLog(#"JSON: %#", responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:#"Error Retrieving Videos"
message:[error localizedDescription]
delegate:nil
cancelButtonTitle:#"Ok"
otherButtonTitles:nil];
[alertView show];
NSLog(#"Error: %#", error);
}];
This does not work for some reason. I get this error:
Error: Error Domain=NSCocoaErrorDomain Code=3840 "The operation couldn’t be completed. (Cocoa error 3840.)"
What could be wrong?!
As mentioned in the comments, this code fails because you are trying to retrieve the result of two URLs joined to one another. The way it is set up now is equivalent to trying to visit http://google.com,http://google.com in a web browser, which will of course fail.
Instead, the solution is to retrieve the results in batches, one after the other. Here's one way to do it:
Write a method which retrieves the YouTube results at a given offset. If you want to retrieve all links starting at 51, then a good idea would be to have a method which takes an offset and returns the results in a completion block.
Write another method which can use the previous one to retrieve the entire list of results. This will need to send multiple network requests to YouTube, one for each batch of 50 that you need, and collect the results somewhere.
There are a couple other issues I noticed in your code sample. One is that you are setting a new request and response serializer in the success block of your network request – instead, you should set these once somewhere in your app, because reallocating them after each request is inefficient. AFHTTPRequestOperationManager does create default instances of these, so you can get away with not setting them at all.
Another potential issue is that you are displaying an alert view in your failure block. Because AFNetworking performs network requests on a background thread by default (from what I recall), you might run into some weird problems (the usual symptom is that your UI will not show up for a few seconds). Apple requires that UI-related methods are called on the main thread.
I'm trying to create an iOS app that uses OAuth2 authentication using the native iOS NSURLSession URL loading classes. I gain an access token fine using the directions here:
http://www.freesound.org/docs/api/authentication.html
I subsequently launch the application and run a search query
https://www.freesound.org/apiv2/search/text/?query=snare
The request header fields looks like this (note my access token is not expired and I have confirmed it is the same as I received from performing the steps above)
{
"Authorization: Bearer" = MY_ACCESS_TOKEN;
}
This fails with:
{"detail": "Authentication credentials were not provided."}
The response headers look like this:
{
Allow = "GET, HEAD, OPTIONS";
Connection = "keep-alive";
"Content-Type" = "application/json";
Date = "Sat, 31 Jan 2015 13:56:32 GMT";
Server = "nginx/1.2.1";
"Transfer-Encoding" = Identity;
Vary = "Accept, Cookie";
"Www-Authenticate" = "Bearer realm=\"api\"";
}
The funny thing is that this does not always happen. If I repeat this entire process a number of times, deleting the app in between, it will eventually work. Once it works, it will continue to work while I'm developing. Sometimes then when I come back to it, say the next day, it stops working and I need to repeat this deleting and re-installing routine to get it back working again!
There's an authentication challenge delegate method on NSURLSession that will get called if implemented. It's a 'server trust' challenge. Could this be something to do with it? Would you even expect an authentication challenge of this nature? There's nothing mentioned about it in the docs alluded to above.
Any help would be much appreciated.
EDIT
This is how the search text ("snare") GET call is made.
I basically pass in an NSMutableURLRequest with the URL set to the above (https://www.freesound.org/apiv2/search/text/?query=snare). useAccessToken is set to YES.
- (void)makeRequest:(NSMutableURLRequest *)request useAccessToken:(BOOL)useAccessToken completion:(CompletionBlock)completion {
NSAssert(completion, #"No completion block.");
if (useAccessToken) {
NSString *accessToken = [[ODMFreesoundTokenCache sharedCache] accessToken];
NSAssert(accessToken.length, #"No access token.");
[request addValue:accessToken forHTTPHeaderField:#"Authorization: Bearer"];
}
NSLog(#"Making request: %# \n\nWith access token: %#", request, [[ODMFreesoundTokenCache sharedCache] accessToken]);
NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSInteger code = [(NSHTTPURLResponse *)response statusCode];
if (code == 200) {
if (!error) {
id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSLog(#"json: %#", json);
completion(json, error);
}
else {
completion(nil, error);
}
}
else {
NSString *reason = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSError *error = [NSError errorWithDomain:#"Request Error" code:code userInfo: reason ? #{NSLocalizedDescriptionKey : reason} : nil];
NSLog(#"error: %#", error);
completion(nil, error);
}
}];
[task resume];
}
The 2 flows for authentication described in the doc are not "safe" for a device. Using API keys would require the secret to be stored in the device.
The OAuth2 flow they support (authorization_code) requires a server to server call to exchange a code for the actual token (This step: http://www.freesound.org/docs/api/authentication.html#step-3). This call requires another credential (the client_secret that you probably should not store in the device either.
You need a server in between that negotiates this for you. Or a server that translates the code flow into token one. (Illustrated here: https://auth0.com/docs/protocols#5).
I'm trying to use a REST API in my iOS app. I know it works because I can make the login request once. Every subsequent request fails with a 401 error. Even if I delete the app from the simulator it still can't be called again until I change the simulator type to one that I haven't used before (i.e. iPad 2, iPhone6, etc.). I can also use a service like https://www.hurl.it to make the same request with the same parameters as many times as I'd like. I'm using AFNetworking and AFHTTPRequestOperationManager. What am I doing wrong?
self.manager = [[AFHTTPRequestOperationManager alloc]initWithBaseURL:[NSURL URLWithString:#"https://api.mydomain.com/"]];
[self.manager POST:#"services/json/user/login" parameters:#{#"username":#"USERNAME", #"password":#"PASSWORD"} success:^(AFHTTPRequestOperation *operation, id responseObject) {
if (![responseObject isKindOfClass:[NSDictionary class]]) { return; }
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
NSDictionary* json = responseObject;
self.sessionName = json[#"session_name"];
self.sessionId = json[#"sessionid"];
[defaults setObject:self.sessionName forKey:#"SessionNameKey"];
[defaults setObject:self.sessionId forKey:#"SessionIDKey"];
if (completion) { completion(); }
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Session request failed: %#", error.userInfo);
}];
If first login called success, you should get some access_token which you use to send along with any subsequent calls.
If API has basic authentication, then you need to pass credentials in HTTP header.
To set credentials in header you can use following methods:
[self.manager.requestSerializer setAuthorizationHeaderFieldWithUsername:#"username" password:#"password"];
(Unauthorised) 401 means you don't have access to the service. Make sure you are trying with correct credentials. Apparently there's nothing wrong with the code.
Update for iOS9 with Swift 2.0 using Bearer authorization
I had the same problem, actually the sessionManager.session.configuration.HTTPAdditionalHeaders get overwritten by the requestSerializer on iOS9, which cause the 401 Access denied error.
Code that was working on iOS8, but not on iOS9:
sm.session.configuration.HTTPAdditionalHeaders = ["Authorization" : "Bearer \(token!)"]
New code for iOS9
sm.requestSerializer.setValue("Bearer \(token!)", forHTTPHeaderField: "Authorization")
I want to upload image to Twitter.
I wrote code as
- (void)postImage:(UIImage *)image withStatus:(NSString *)status
{
NSLog(#"Share on Twitter");
ACAccountType *twitterType =
[self.accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];
SLRequestHandler requestHandler =
^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
if (responseData) {
NSInteger statusCode = urlResponse.statusCode;
if (statusCode >= 200 && statusCode < 300) {
NSDictionary *postResponseData =
[NSJSONSerialization JSONObjectWithData:responseData
options:NSJSONReadingMutableContainers
error:NULL];
NSLog(#"[SUCCESS!] Created Tweet with ID: %#", postResponseData[#"id_str"]);
}
else {
NSLog(#"[ERROR] Server responded: status code %d %#", statusCode,
[NSHTTPURLResponse localizedStringForStatusCode:statusCode]);
}
}
else {
NSLog(#"[ERROR] An error occurred while posting: %#", [error localizedDescription]);
}
};
ACAccountStoreRequestAccessCompletionHandler accountStoreHandler =
^(BOOL granted, NSError *error) {
if (granted) {
NSArray *accounts = [self.accountStore accountsWithAccountType:twitterType];
NSURL *url = [NSURL URLWithString:#"https://api.twitter.com"
#"/1.1/statuses/update_with_media.json"];
NSDictionary *params = #{#"status" : status};
SLRequest *request = [SLRequest requestForServiceType:SLServiceTypeTwitter
requestMethod:SLRequestMethodPOST
URL:url
parameters:params];
NSData *imageData = UIImageJPEGRepresentation(image, 1.f);
[request addMultipartData:imageData
withName:#"media[]"
type:#"image/jpeg"
filename:#"image.jpg"];
[request setAccount:[accounts lastObject]];
[request performRequestWithHandler:requestHandler];
}
else {
NSLog(#"[ERROR] An error occurred while asking for user authorization: %#",
[error localizedDescription]);
}
};
[self.accountStore requestAccessToAccountsWithType:twitterType
options:NULL
completion:accountStoreHandler];
}
I am getting error as
The operation couldn’t be completed. (com.apple.accounts error 6.)
You should use the SLComposeViewController class. In order to do this, a number of steps should be performed in sequence. Firstly, the application may optionally check to verify whether a message can be sent to the specified social network service. This essentially equates to checking if a valid social network account has been configured on the device and is achieved using the isAvailableForServiceType: class method, passing through as an argument the type of service required from the following options:
SLServiceTypeFacebook
SLServiceTypeTwitter
SLServiceTypeSinaWeibo
The following code, for example, verifies that Twitter service is available to the application:
if ([SLComposeViewController isAvailableForServiceType:SLServiceTypeTwitter])
{
// Device is able to send a Twitter message
}
This method call is optional and, in the event that an account for the specified social network has yet to be set up, the composer will simply take the user to the device’s Settings application where a Twitter account may be configured.
The next step is to create an instance of the SLComposeViewController class and supply an optional completion handler to be called when the composer screen is either cancelled by the user or used to send a message. Next, a range of methods may be called on the instance to initialize the object with the content of the message, including the initial text of the message, an image attachment and a URL:
setInitialText: - Sets the initial text of the message on the SLComposeViewController instance.
addImage: - Adds image files as attachments to the message.
addURL: - Adds a URL to the message. The method automatically handles the URL shortening.
Each of the above methods returns a Boolean result indicating whether the addition of content was successful.
Finally, when the message is ready to be presented to the user, the SLComposeViewController object is presented modally by calling the presentViewController: method of the parent view controller:
SLComposeViewController *composeController = [SLComposeViewController
composeViewControllerForServiceType:SLServiceTypeTwitter];
[composeController setInitialText:#"Just found this great website"];
[composeController addImage:postImage.image];
[composeController addURL: [NSURL URLWithString:
#"http://www.website.com"]];
[self presentViewController:composeController
animated:YES completion:nil];
Once called, this method will present the composer view to the user primed with any text, image and URL contents pre-configured via the method calls. Once displayed, the user has the option to modify the text of the message, cancel the message, add location data or send the message. If a completion handler has been configured it will be called and passed a value indicating the action that was taken by the user within the composer view. Possible values are:
SLComposeViewControllerResultCancelled – The user cancelled the composition session by touching the Cancel button.
SLComposeViewControllerResultDone – The user sent the composed message by touching the Send button.
I am working on an iOS application that will use RestKit 0.20 to make REST-based calls to a service that is running on JBoss AS 7.1.1 and using restEASY as its REST-based web service framework.
The REST service that the client app will be calling is used to retrieve objects based on their unique identifier. Since these objects can be small or large (> 1MB in size) and great in number (20? 50? 100 or more at a time) I don't want to make one large call to retrieve them all at once. Rather, I was planning on using RestKit's queued operation support to create a GET request for each object based on the object identifier, and execute the calls asynchronously. Once the GET has completed, each object will be processed through the use of Objective-C blocks so as to avoid any unnecessary blocking.
My RestKit client code looks like this...
NSArray *identifiers = ...
RKObjectManager *objectManager = [RKObjectManager sharedManager];
RKResponseDescriptor *getObjResp = [RKResponseDescriptor responseDescriptorWithMapping:[MyObject mapping] pathPattern:[WebServiceHelper pathForServiceOperation:#"/objects/:uniqueIdentifier"] keyPath:nil statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)];
for (int i=0; i < identifiers.count; i++) {
NSString *identifier = [identifiers objectAtIndex:i];
NSURL *serviceURL = [WebServiceHelper urlForServiceOperation:[NSString stringWithFormat:#"/objects/%#", identifier]];
NSURLRequest *request = [NSURLRequest requestWithURL:serviceURL];
RKObjectRequestOperation *requestOp = [[RKObjectRequestOperation alloc] initWithRequest:request responseDescriptors:#[getObjResp]];
[requestOp setCompletionBlockWithSuccess:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
MyObject *obj = [mappingResult firstObject];
if (self.delegate != nil) {
[self.delegate didLoadObjectWithIdentifier:identifier myObj:obj];
}
} failure:^(RKObjectRequestOperation *operation, NSError *error){
if (self.delegate != nil) {
[self.delegate didFinishWithError:error];
}
}];
[objectManager enqueueObjectRequestOperation:requestOp];
}
From there, the delegate method that gets called when an object has been retrieved looks like this:
-(void)didLoadObjectWithIdentifier:(NSString *)identifier myObj:(MyObject *)myObj {
if(secureMessage != nil) {
NSLog(#"Message %# retrieved successfully : %#:%#", identifier, myObj);
} else {
NSLog(#"NO OBJ");
}
}
The calls appear to be functioning as expected, as I am able to print out information about the retrieve objects. However, I am seeing some weird/unexepcted behavior on the service side.
First, I see a number of Exceptions being thrown by restEASY:
13:22:02,903 WARN [org.jboss.resteasy.core.SynchronousDispatcher] (http--0.0.0.0-8080-10) Failed executing GET /objects/BBFE39EA126F610C: org.jboss.resteasy.spi.WriterException: ClientAbortException: java.net.SocketException: Broken pipe
at org.jboss.resteasy.core.ServerResponse.writeTo(ServerResponse.java:262) [resteasy-jaxrs-2.3.2.Final.jar:]
at org.jboss.resteasy.core.SynchronousDispatcher.writeJaxrsResponse(SynchronousDispatcher.java:585) [resteasy-jaxrs-2.3.2.Final.jar:]
at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:506) [resteasy-jaxrs-2.3.2.Final.jar:]
at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:119) [resteasy-jaxrs-2.3.2.Final.jar:]
at org.jboss.seam.resteasy.ResteasyResourceAdapter$1.process(ResteasyResourceAdapter.java:145) [jboss-seam-resteasy.jar:2.3.0.Final]
at org.jboss.seam.servlet.ContextualHttpServletRequest.run(ContextualHttpServletRequest.java:65) [jboss-seam.jar:2.3.0.Final]
at org.jboss.seam.resteasy.ResteasyResourceAdapter.getResource(ResteasyResourceAdapter.java:120) [jboss-seam-resteasy.jar:2.3.0.Final]
...
It would appear as though RestKit is closing the socket somehow (or some other error is preventing the object from being read from the server). I am unable to find anything in the documentation that could explain what is going on here.
Secondly, though, I also see another call for the very same object when a request fails with this error. Why is the GET being called more than once? Is RestKit redoing the failed GET request?
I'm mostly concerned about why the Exception is occurring within restEASY, as it will make it difficult to diagnose calls that really do fail. Has anyone seen this behavior before? Any tips as to how I can correct these issues? Thanks for any help you can give!!
Those exception are resulted from disconnected Clients i.e. some of the users might quit the app while waiting for the process to complete OR has a network failure (at the client end).
Hence, Broken Pipe.