AFNetworking: Append parameters as the query string for PUT request - ios

First of all, I realize that for a PUT request the request parameters should be passed in the request body. However, I am working with an API (which I am only consuming, not developing) that expects the request parameters to be appended as the query string for a PUT request.
I am making use of a subclass of AFHTTPClient. For the particular PUT request that I am referring to, I make use of getPath:parameters:success:failure:.
The solution that I have found so far is manually constructing the path variable to include the parameters I want to pass. Of course, this is not ideal and I was wondering if there is another option that is less error prone.
In short, is there a way to send a PUT request using AFHTTPClient (a subclass of) with the passed parameters appended (and encoded) as the query string (just like a GET request)?

The getPath:parameters:success:failure method inside AFHTTPClient.m calls requestWithMethod:path:parameters. Inside the latter method, the HTTP method is checked against certain values to decide how to append the parameters to the request. As you can see, by default, the parameters should only be appended to the URL in case of a GET, HEAD or DELETE request. Since you need them to be appended to the URL in case of a PUT request too, modify the requestWithMethod:path:parameters like this:
- (NSMutableURLRequest *)requestWithMethod:(NSString *)method
path:(NSString *)path
parameters:(NSDictionary *)parameters
{
NSURL *url = [NSURL URLWithString:path relativeToURL:self.baseURL];
NSMutableURLRequest *request = [[[NSMutableURLRequest alloc] initWithURL:url] autorelease];
[request setHTTPMethod:method];
[request setAllHTTPHeaderFields:self.defaultHeaders];
if ([method isEqualToString:#"GET"] || [method isEqualToString:#"HEAD"]) {
[request setHTTPShouldUsePipelining:YES];
}
if (parameters) {
if ([method isEqualToString:#"GET"] || [method isEqualToString:#"HEAD"] || [method isEqualToString:#"DELETE"] || [method isEqualToString:#"PUT"]) {
url = [NSURL URLWithString:[[url absoluteString] stringByAppendingFormat:[path rangeOfString:#"?"].location == NSNotFound ? #"?%#" : #"&%#", AFQueryStringFromParametersWithEncoding(parameters, self.stringEncoding)]];
[request setURL:url];
} else {
NSString *charset = (NSString *)CFStringConvertEncodingToIANACharSetName(CFStringConvertNSStringEncodingToEncoding(self.stringEncoding));
switch (self.parameterEncoding) {
case AFFormURLParameterEncoding:;
[request setValue:[NSString stringWithFormat:#"application/x-www-form-urlencoded; charset=%#", charset] forHTTPHeaderField:#"Content-Type"];
[request setHTTPBody:[AFQueryStringFromParametersWithEncoding(parameters, self.stringEncoding) dataUsingEncoding:self.stringEncoding]];
break;
case AFJSONParameterEncoding:;
[request setValue:[NSString stringWithFormat:#"application/json; charset=%#", charset] forHTTPHeaderField:#"Content-Type"];
[request setHTTPBody:[AFJSONStringFromParameters(parameters) dataUsingEncoding:self.stringEncoding]];
break;
case AFPropertyListParameterEncoding:;
[request setValue:[NSString stringWithFormat:#"application/x-plist; charset=%#", charset] forHTTPHeaderField:#"Content-Type"];
[request setHTTPBody:[AFPropertyListStringFromParameters(parameters) dataUsingEncoding:self.stringEncoding]];
break;
}
}
}
return request;
}

You can just do what the code in datwalk's answer is doing without modifying the underlying AFNetworking code. Use AFNetworking to create a path that includes URL parameters:
NSDictionary *mutableParameters = [[NSMutableDictionary alloc] initWithObjectsAndKeys:#"csv",#"format", #"0",#"level", #"2013-10-25", #"keydate", nil];
NSString *urlPath = [NSString stringWithFormat:#"applications/%#/Planning?%#", name, AFQueryStringFromParametersWithEncoding(mutableParameters, NSUTF8StringEncoding)];
NSString *apiFunctionPath = [urlPath stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
[self putPath:apiFunctionPath
parameters:nil
success:^(AFHTTPRequestOperation *operation, id XML) {
NSLog(#"%#",XML);
}
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"%#",error);
}];

For the case of my API, changing the encoding of the AFHTTPClient object solved the issue:
myAFHTTPClient.parameterEncoding = AFJSONParameterEncoding;

Related

Fetching an access token for youtube api iOS "Error" : "invalid_request"

I'm trying to get an access token for a youtube app for iOS. Here's the relevant code I have been using from my viewDidLoad method:
mAuth = [[GTMOAuth2Authentication alloc]init];
[mAuth setClientID:#"<MY CLIENT ID>"];
[mAuth setClientSecret:#"<MY CLIENT SECRET>"];
[mAuth setRedirectURI:#"urn:ietf:wg:oauth:2.0:oob"];
[mAuth setScope:#"https://gdata.youtube.com"];
[self.web loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:#"https://accounts.google.com/o/oauth2/auth?client_id=%#&redirect_uri=%#&scope=%#&response_type=code&access_type=offline", mAuth.clientID, mAuth.redirectURI, mAuth.scope]]]];
After this is called, the user has to grant access to their account, then I retrieve the auth code from the resulting page in the following code:
NSString *string = [self.web stringByEvaluatingJavaScriptFromString:#"document.title"];
if([string rangeOfString:#"Success"].location != NSNotFound){
NSLog(#"This is the code page");
NSString *importantCode = [[string componentsSeparatedByString:#"="] objectAtIndex:1];
NSLog(#"%#", importantCode);
if([self.defaults objectForKey:#"super important code"] == nil){
[self.defaults setObject:importantCode forKey:#"super important code"];
[self.defaults synchronize];
NSString *post = [NSString stringWithFormat:#"code=%#&client_id=%#&client_secret=%#&redirect_uri=%#&grant_type=code", [self.defaults objectForKey:#"super important code"], mAuth.clientID, mAuth.clientSecret, mAuth.redirectURI];
NSData *postData = [post dataUsingEncoding:NSASCIIStringEncoding];
NSString *postLength = [NSString stringWithFormat:#"%d", [postData length]];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc]init];
[request setURL:[NSURL URLWithString:[NSString stringWithFormat:#"https://accounts.google.com/o/oauth2/token"]]];
[request setHTTPMethod:#"POST"]; [request setValue:postLength forHTTPHeaderField:#"Content-Length"];
[request setValue:#"application/x-www-form-urlencoded" forHTTPHeaderField:#"Content-Type"];
[request setHTTPBody:postData];
[self.web loadRequest:request];
}
[timer invalidate];
}
After that, I should be given the access token in response to my POST request, but instead I get a page that simply says:
{
"error" : "invalid_request"
}
Does anyone (see where I went wrong)/(know how to retrieve the access token)?
I’d be interested in knowing what the HTTP status code is... I suspect ”invalid_request” means 400, which almost always means there’s some irritating stupid little error in the way you composed your authent URI. I don’t know enough iOS/ObcJ to tell if you’ve properly URLescaped any funny characters in any of the parameter values, but it’s worth checking. Or a typo in one of the values, or an extra newline creeping in or something?
It was as simple as this, I had set the grant_type parameter to 'code' when it should have been 'authorisation_code' if you are reading this and you are using the youtube api, don't make the same mistake. If you're reading this and you're sending a POST request in general, this error "invalid_request" means that either you skipped one of the parameters you should have added, or you added it incorrectly.

Upgrading to AFNetworking

I am taking over an old iOS project from developers no longer part of the project - the app is getting a rewrite and with that I am going to support iOS7 and upwards only.
So, I wanted to use AFNetworking 2.0 instead of ASIHTTPRequest - the reason behind this is NSURLSeesion. AFNetworking 2.0 supports NSURLSession and with that I can get my app to download content in the background at opportunistic times (According to Apple - NSURLSession must be used and Background Fetch mode turned on, for this to work? )
Let me start out by saying I am a new developer to iOS and networking stuff goes a little over my head - but I am determined to learn more about it and as much as I can. I have read AFNetworking documentation as well, but I fear since some of the terminology escapes me (Request, Response, Sterilisation, etc) - I am not grasping them 100%.
So, I took a look at the ASIHTTPRequest code the previous developer used to, from what I can see, build a GET / POST request - This is the code they used:
+ (ASIHTTPRequest*) buildRequest: (NSString*) url RequestType: (NSString*) requestType
PostData: (NSString*) postData
Host: (NSString*) host
ContentType: (NSString*) contentType
SoapAction: (NSString*) soapAction
RequestProperties: (NSDictionary*) requestProperties
{
NSURL *url = [NSURL URLWithString: url];
ASIHTTPRequest *request = [[[ASIHTTPRequest alloc] initWithURL:u] autorelease];
[request setDidFinishSelector:#selector(requestDone:)];
[request setDidFailSelector:#selector(requestWentWrong:)];
[request setTimeOutSeconds:20];
[request setQueuePriority:NSOperationQueuePriorityVeryHigh];
if (host != nil)
[request addRequestHeader: #"Host" value: host];
if (contentType != nil)
[request addRequestHeader: #"Content-Type" value: contentType];
if (soapAction != nil)
[request addRequestHeader: #"SOAPAction" value:soapAction];
if (requestType != nil)
[request setRequestMethod: requestType];
if (postData != nil)
{
NSMutableData* mPostData = [NSMutableData dataWithData:[postData dataUsingEncoding:NSUTF8StringEncoding]];
NSString *msgLength = [NSString stringWithFormat:#"%d", [postData length]];
[request setPostBody: mPostData];
[request addRequestHeader: #"Content-Length" value:msgLength];
}
if (requestProperties != nil)
{
for (int i = 0; i < [[requestProperties allKeys] count]; i++)
{
[request addRequestHeader:[[requestProperties allKeys] objectAtIndex: i] value: [requestProperties objectForKey:[[requestProperties allKeys] objectAtIndex: i]]];
}
}
return request;
}
I'm trying to understand this code and upgrade it to use AFNetworking V2.0 instead. I assume, just replacing ASIHTTPRequest with AFHTTPRequestOperation will not do the trick, correct?
I have been given some help and also managed to do a lot of digging around to see how I can get this right.
I made the method simpler - as I did not need Soap / Content-type, etc - just urlParamerters and some basic stuff:
This is the answer I came up with:
+ (AFHTTPSessionManager *) buildRequest: (NSString*) url RequestType: (NSString*) requestType PostDataValuesAndKeys: (NSDictionary*) postData RequestProperties: (NSDictionary*) requestProperties
{
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
if ([requestType isEqualToString:#"GET"])
{
[manager GET:url parameters:postData success:^(NSURLSessionDataTask *dataTask, id responseObject){
//Success
NSLog (#"Success");
NSData *xmlData = responseObject;
NSLog(#"Got XML Data: %#", xmlData);
}
failure:^(NSURLSessionDataTask *dataTask, NSError *error){
//Failure
NSLog (#"Failure");
}];
}else if ([requestType isEqualToString:#"GT"]){
[manager POST:url parameters:postData success:^(NSURLSessionDataTask *dataTask, id responseObject){
//Success
}
failure:^(NSURLSessionDataTask *dataTask, NSError *error){
//Failure
NSLog (#"Failure");
}];
}
return manager;
}
It will work for what I need it to do - but I am not sure if it's the best way to do it.
I couldn't see how I could detect the requestType other thank with looking at the NSString value. I looked into the AFHTTPSessionManager.h file for some clues on what to do with that - Matt suggests overriding the GET / POST methods if I want them done differently - per his comments in the header file:
Methods to Override
To change the behavior of all data task operation construction, which
is also used in the GET / POST / et al. convenience methods,
override dataTaskWithRequest:completionHandler:.
Also there is a requestSerializer property in that file - which you could use to detect the type of request - however it's implementation goes to the super class: AFURLSessionManager
In that class - there is a requestWithMethodmethod.
So, I tried to do this instead:
If I try implement that method - then I am not using the convince methods in AFHTTPSessionManager:
(NSURLSessionDataTask *)GET:(NSString *)URLString
parameters:(NSDictionary *)parameters
success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure;
Unless I have that completely wrong. After that I decided to just check the requestType using [NSString isEqualToString]

How to add an Auth Token in every request using AFIncrementalStore?

I have an iOS + Rails 3.1 app, and I'm using AFIncrementalStore for the client-server communication.
I have implemented Token Authentication on my Rails server according to this tutorial: http://matteomelani.wordpress.com/2011/10/17/authentication-for-mobile-devices/
I now want to include the &auth_token=XXXXXXXX in every request from client to server, including POST requests. How would I do that? I haven't found the solution in this related post: Using AFIncrementalStore with an Auth token
UPDATE: this is my first code attempt, but doesn't seem to send the auth_token:
(inside my AFIncrementalStoreHTTPClient sub-class)
- (NSMutableURLRequest *)requestForFetchRequest:(NSFetchRequest *)fetchRequest withContext:(NSManagedObjectContext *)context {
NSMutableURLRequest *request = [[super requestForFetchRequest:fetchRequest withContext:context] mutableCopy];
NSMutableString *requestBody = [[NSMutableString alloc] initWithData:[request HTTPBody] encoding:NSUTF8StringEncoding];
[requestBody appendFormat:#"&%#=%#", #"auth_token", #"xkT2eqqdoNp5y4vQy7xA"];
[request setHTTPBody:[requestBody dataUsingEncoding:NSUTF8StringEncoding]];
return request;
}
UPDATE: I skimmed your question (sorry!), and my sample code below works for a regular AFHTTPClient, but not AFIncrementalStore. The same basic approach will work, though, and there's sample code at this answer that should point you in the right direction.
You can't just append &auth_token=whatever to the end of your HTTP body in all cases.
You probably want to override your getPath... and postPath... methods with something like:
- (void)getPath:(NSString *)path
parameters:(NSDictionary *)parameters
success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
if (parameters) {
// Make a mutable copy and add the "token" parameter to the dictionary
NSMutableDictionary *mutableParams = [parameters mutableCopy];
[mutableParams setObject:#"whatever" forKey:#"token"];
parameters = [NSDictionary dictionaryWithDictionary:mutableParams];
} else {
parameters = #{#"token" : #"whatever"};
}
[super getPath:path parameters:parameters success:success failure:failure];
}
This approach will allow AFNetworking to appropriately encode your parameters depending on your specific request and encoding settings.
If you are rolling your own AFHTTPRequestOperation objects instead of using the convenience methods (you probably aren't), just make sure you include the token in parameters before you create your NSURLRequest like so:
NSURLRequest *request = [self requestWithMethod:#"GET" path:path parameters:parameters];

iOS: using async

I currently have a screen with 2 tables. I'm getting the data synchronously and putting it on the screen. Code looks something like:
viewController.m
DBAccess_Error_T = [getList:a byCompanyID:1];
DBAccess_Error_T = [getList:b byCompanyID:2];
[self putListAOnScreen];
[self putListBOnScreen];
DBAccess.m
+ (DBAccess_Error_T)getList:(NSMutableArray*)a byCompanyID:(NSInteger)cID
{
// Pack this up in JSON form
[self queryDB:postData];
// Unpack and put it into variable a
}
+ (id)queryDB:(id)post
{
// Send request
// Get back data
}
I'm now trying to switch this over to async and I'm struggling. It's been hard even with website tutorials and documentations.
Since all of my database utilities are in separate files from the viewControllers, I'm not sure how I can use the didReceiveData and didReceiveResponse handlers. Also, since I have 2 arrays to fill for my 2 tables, how do I distinguish the difference in didReceiveData?
Instead, what I'm trying to do now is use sendAsynchronousRequest, but it seems I need to create an unpack function for every send function...let me know if I'm way off here...it looks something like:
viewController.m stays the same
DBAccess.m
+ (DBAccess_Error_T)getList:(NSMutableArray*)a byCompanyID:(NSInteger)cID
{
NSDictionary *post = /*blah blah*/
[self queryDB:post output:(a)];
}
+ (id)queryDB:(id)post output:(id)output
{
NSError *error;
NSData *jsonPayload = [NSJSONSerialization dataWithJSONObject:post options:NSJSONWritingPrettyPrinted error:&error];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
[theRequest setHTTPMethod:#"POST"];
[theRequest setHTTPBody:jsonPayload];
[NSURLConnection sendAsynchronousRequest:request
queue:[[NSOperationQueue alloc] init]
completionHandler:^(NSURLResponse *response,
NSData *data,
NSError *error)
{
if ([data length] >0 && error == nil)
{
[self unpackDataForList:output data:data]; // This function needs to be different depending on which function called queryDB...the data will be unpacked in a different way
}
}
}
+ (void)unpackDataForList:(id)output data:(NSData*)data
{
// Do my unpacking here and stick it into 'output'.
}
How can I call a different unpackData function? are function pointers the right way to do this? Is this approach way off? Any tips would be greatly appreciated!
Have you ever looked at ASIHTTPRequest? It makes your life a lot easier by allowing you to use blocks. Here's an example of how to make an asynchronous request:
- (IBAction)grabURLInBackground:(id)sender
{
NSURL *url = [NSURL URLWithString:#"http://allseeing-i.com"];
__block ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setCompletionBlock:^{
// Use when fetching text data
NSString *responseString = [request responseString];
// Use when fetching binary data
NSData *responseData = [request responseData];
}];
[request setFailedBlock:^{
NSError *error = [request error];
}];
[request startAsynchronous];
}
You can find more information here:
http://allseeing-i.com/ASIHTTPRequest/

Send request to server iOS

I am trying to figure out how to send data to the server but currently without any progress.
What I know
On the server I have got some php script that return me data in response
for example with this URL: http://test.com/mobile_api/register
this script get next parameters:
id
name
info
time
so the string which I need looking like below
http://test.com/mobile_api/register?id=1000&name=alex&info=1.0&time=10000
What is best way to send this is string on the server
Now I'm trying to use ASIHTTPRequest. Can anybody send an example how to create correct request with my parameters.
This sample code should help you
-(void)sendRequest
{
int userId=10, time=10000;
NSString *name = #"ABC";
float info = 1.0;
NSString *urlString = [NSString stringWithFormat:#"http://test.com/mobile_api/register?id=%d&name=%#&info=%f&time=%d",userId,name,info,time];
ASIHTTPRequest *request = [[ASIHTTPRequest alloc] initWithURL:[NSURL URLWithString:urlString]];
//You need to add ASIHTTPRequestDelegate in header(.h) file
[request setDelegate:self];
[request startAsynchronous];
}
- (void)requestFinished:(ASIHTTPRequest *)request
{
// Use when fetching text data
NSString *responseString = [request responseString];
// Use when fetching binary data
NSData *responseData = [request responseData];
}
- (void)requestFailed:(ASIHTTPRequest *)request
{
NSError *error = [request error];
}

Resources