When I investigate this method in xcode debug mode, a few strange things happen when constructing the request.
Extra characters get added to the urlString. For instance, if SessionId = "abc", then after executing the line starting at NSURLRequest *request..., the debugger shows that SessionUrlString = "...session/abc\x03" instead of simply "...session/abc". This is despite the fact that the debugger still shows SessionId = "abc". Why is this?
The request object doesn't appear to contain the url
anywhere, even though its constructor just took that url as a variable. Where did it go? Is it stored in the request object somewhere in the AFHTTPCLient object?
-(NSObject*)makeRequestForSessionUsingId: (NSString *)SessionId{
NSString *baseSessionURLString = [kCwAPIBaseURLString stringByAppendingString:#"session/"];
NSString *SessionURLString = [baseSessionURLString stringByAppendingString:SessionId];
NSURL *url = [NSURL URLWithString:SessionURLString];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
__block NSObject *sessionJSON = [[NSObject alloc] init];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
NSLog(#"IP Address: %#", [JSON valueForKeyPath:#"origin"]);
sessionJSON = JSON;
} failure:nil];
[operation start];
return sessionJSON;
}
I'm not sure what's causing the trailing characters in your URL string but if you can output it in an NSLog statement and it shows up correctly it's probably correct.
NSURLRequest stores the URL you create it with in it's - URL property which you can see in the documentation. This means you should be able to use something like:
NSLog(#"Request URL: %#", [request URL]);
To see the URL.
The bigger issue I see here is your return pattern. If this AFJSONRequestOperation takes any amount of time the value that is returned will be an empty sessionJSON variable. When you run [operation start]; the operation does not, by default, wait to complete before returning the value you specify. There is a way to make it wait but you never want to block any threads waiting for a network request that could take longer than you hope. You have some other options here for what you could do but mainly you have to think about it in a different way. Pretty much everything you want to do with the response from the network request needs to be done in the success and failure blocks. This method should not try and return a value (unless it's in the form of another block). Some ways you could do this:
Store the response JSON in a #property on your class. After doing that call a method [self foo]; that then uses the stored response to do what you want.
In the block call a method passing the response json [self foo:JSON]
Post an NSNotification with the response as an object.
Pass a block to this method that you then call passing the response back to the original caller.
Related
I have the following scenario in an application that uses AFNetworking to make services calls:
I call a special service that will generate a token for me
I call the service that I want, sending this token as a parameter
I call another special service to destroy the token.
I have to follow these 3 steps every time I make a request to the server. I cannot change the way the server works, so I have to comply to this requirement. I also cannot use the same token for more than one request.
My question is the following - I tried to accomplish this using AFHTTPRequestOperations:
NSError *serializationError = nil;
NSMutableURLRequest *request = [self.manager.requestSerializer requestWithMethod:#"POST" URLString:[[NSURL URLWithString:#"serviceName.json" relativeToURL:self.manager.baseURL] absoluteString] parameters:#{ #"token": token } error:&serializationError];
AFHTTPRequestOperation *myRequestOperation = [self.manager HTTPRequestOperationWithRequest:request success:^(AFHTTPRequestOperation * _Nonnull operation, id _Nonnull responseObject) {
// Success login
} failure:^(AFHTTPRequestOperation * _Nonnull operation, NSError * _Nonnull error) {
// Failure logic
}];
[myRequestOperation addDependency:createTokenRequestOperation];
where self.manager is an instance of AFHTTPRequestOperationManager, but there is a problem - I do not have a value for token.
Since myRequestOperation should execute only after point 1 from the list above, I make it dependent on the operation that will get me a token.
Now comes my confusion - how can I create an operation that uses a parameter from a previous operation, when I need to have both of them instantiated in order to make the one depend on the other?
Since I was not able to find a solution that will work for me, I ended up using PromiseKit, which allows me to chain asynchronous calls like this:
[NSURLConnection promise:rq1].then(^(id data1){
return [NSURLConnection promise:rq2];
}).then(^(id data2){
return [NSURLConnection promise:rq3];
}).then(^(id data3){
// Work with the data returned from rq3
});
I am trying to solve the following problem:
I am sending a POST request with some additional header values to a specific URL. For that purpose I use NSMutabeURLRequest. It works nice when i NSLog the response, but I also need the URL of the redirect. If I use something like request.URL in the competitionHandler it returns the URL i sent my POST request to, it's not what I need.
Any tips on how to get the URL of the redirect? (It would be nice if it won't change my code significantly.
Below is what I have so far:
url = [NSURL URLWithString:#"https://***"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:#"POST"];
[request setValue:#"application/json; charset=utf-8" forHTTPHeaderField:#"Content-Type"];
//Some additional values are set here
[request setHTTPBody:data];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
NSLog(#"%#", response);
if (error)
NSLog(#"%s: NSURLConnection error: %#", __FUNCTION__, error);
NSString *responseString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"responseString: %#",responseString);
}];
From Apple's Developer Docs:
A redirect occurs when a server responds to a request by indicating that the client should make a new request to a different URL. The NSURLSession, NSURLConnection, and NSURLDownload classes notify their delegates when this occurs.
To handle a redirect, your URL loading class delegate must implement one of the following delegate methods:
For NSURLSession, implement the URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler: delegate method.
For NSURLConnection, implement the connection:willSendRequest:redirectResponse: delegate method.
For NSURLDownload, implement the download:willSendRequest:redirectResponse: delegate method.
In these methods, the delegate can examine the new request and the response that caused the redirect, and can return a new request object through the completion handler for NSURLSession or through the return value for NSURLConnection and NSURLDownload.
The delegate can do any of the following:
Allow the redirect by simply returning the provided request.
Create a new request, pointing to a different URL, and return that request.
Reject the redirect and receive any existing data from the connection by returning nil.
In addition, the delegate can cancel both the redirect and the connection. With NSURLSession, the delegate does this by sending the cancel message to the task object. With the NSURLConnection or NSURLDownload APIs, the delegate does this by sending the cancel message to the NSURLConnection or NSURLDownload object.
The delegate also receives the connection:willSendRequest:redirectResponse: message if the NSURLProtocol subclass that handles the request has changed the NSURLRequest in order to standardize its format, for example, changing a request for http://www.apple.com to http://www.apple.com/. This occurs because the standardized, or canonical, version of the request is used for cache management. In this special case, the response passed to the delegate is nil and the delegate should simply return the provided request.
So basically, you need to make use of the connection:willSendRequest:redirectResponse: delegate method to grab the new url and perform whatever operation you need to on it
The problem is that you're using the sendAsynchronousRequest:queue:completionHandler: method, which uses a completion handler and doesn't take a delegate.
After you implement the delegate method, you'll need to use NSURLConnection's initWithRequest:delegate: or initWithRequest:delegate:startImmediately: method to create the NSURLConnection object instead. You'll also have to implement a delegate method to obtain the data returned by the request.
Alternatively, if you don't have to support anything older than iOS 7 or OS X v10.9, NSURLSession provides the ability to use delegate methods for handling redirection even in combination with methods that take a completion callback.
I need to get image information from server, such image name, image id. Then use image id as one of parameters to make post, get image actual data. More specific, there are three images I should get.
First, I use getImageInfo to get image information.
- (void)getImageInfo {
// compose request
NSUserDefaults *getUserInfo = [NSUserDefaults standardUserDefaults];
NSString *uid = [getUserInfo objectForKey:#"uid"];
NSString *checkCode = [getUserInfo objectForKey:#"checkCode"];
NSString *data = [NSString stringWithFormat:#"uid=%#&yangzhengma=%#", uid, checkCode];
NSURL *url = [NSURL URLWithString:#"http://121.199.35.173:8080/xihuan22dcloud/services/Shibietupianservice/serviceGetallshibietu"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPBody = [data dataUsingEncoding:NSUTF8StringEncoding];
request.HTTPMethod = #"POST";
[[self.session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){
if (!error) {
NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response;
if (httpResp.statusCode == 200) {
// parse data in ram and put into images' imageInfos array
[self.images parseImageInfo:[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]];
[self getImageRawData];
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
}
}
}] resume];}
Then I use getImageRawData to get three image data.
- (void)getImageRawData {
// compose request dynamically
NSUserDefaults *getUserInfo = [NSUserDefaults standardUserDefaults];
NSString *uid = [getUserInfo objectForKey:#"uid"];
NSString *checkCode = [getUserInfo objectForKey:#"checkCode"];
NSURL *url = [NSURL URLWithString:#"http://121.199.35.173:8080/xihuan22dcloud/services/Shibietupianservice/serviceGetthetupian"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = #"POST";
NSInteger count = 0;
for (ImageInformation *temp in self.images.imageInfos) {
NSString *data = [NSString stringWithFormat:#"uid=%#&yangzhengma=%#&tupianid=%#", uid, checkCode, temp.imageId];
request.HTTPBody = [data dataUsingEncoding:NSUTF8StringEncoding];[[self.session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){
// if client side is no errors, continue
if (!error) {
// if server side is no errors, continue
NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response;
if (httpResp.statusCode == 200) {
NSLog(#"图片内容:%#", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
// in ram and put into images' imageRawData array
[self.images parseImageRawData:[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] withImageId:temp.imageId withIndex:count];
// store data to disk
// NSString *path = [[NSString alloc] initWithFormat:#"image%#", temp.imageId];
// [FCFileManager writeFileAtPath:path content:data];
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
}
}
}] resume];
count++;
}}
Here, it will loop three times, three responses come back, only the last one is complete, the others carry a error message, or incomplete raw data sometimes. Now I'm diving into concurrency programming guide, I guess serial queue likely can solve this problem.
Output like this:
2014-12-16 22:38:48.739 WeddingNewVersion[997:83366] 图片内容:<ns:serviceGetthetupianResponse xmlns:ns="http://serviceimpl.my.com"><ns:return>error</ns:return></ns:serviceGetthetupianResponse>
2014-12-16 22:38:48.749 WeddingNewVersion[997:83366] 图片内容:<ns:serviceGetthetupianResponse xmlns:ns="http://serviceimpl.my.com"><ns:return>error</ns:return></ns:serviceGetthetupianResponse>
2014-12-16 22:38:51.943 WeddingNewVersion[997:83366] 图片内容:<ns:serviceGetthetupianResponse xmlns:ns="http://serviceimpl.my.com"><ns:return>/9j/...(complete data)...9k=%%226654474.0</ns:return></ns:serviceGetthetupianResponse>
parameters of requests:
2014-12-17 14:59:25.364 WeddingNewVersion[1875:226651] uid=6&yangzhengma=odWoDXWcBv1jOrEhywkq7L&tupianid=41
2014-12-17 14:59:25.368 WeddingNewVersion[1875:226651] uid=6&yangzhengma=odWoDXWcBv1jOrEhywkq7L&tupianid=42
2014-12-17 14:59:25.368 WeddingNewVersion[1875:226651] uid=6&yangzhengma=odWoDXWcBv1jOrEhywkq7L&tupianid=43
the problem is likely not in composing request.
------------------------------------------------update1-----------------------------------------------
I have tried to put data task of session into a serial queue. Disappointed, this is not working.
dispatch_async(self.serialQueue, ^{
[[self.session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){...}] resume];
});
Meanwhile, I make delegateQueue of session as nil, reference says if nil, the session creates a serial operation queue for performing all delegate method calls and completion handler calls.
Now I am still confused how to make it right.
-----------------------------------------------update2------------------------------------------------
I add [NSThread sleepForTimeInterval:0.5] into the block dispatched to serial queue.
dispatch_async(self.serialQueue, ^{
[[self.session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){...}] resume];
[NSThread sleepForTimeInterval:0.5];
});
It does not work. The three responses are complete, but they are all the same.
Thank you in advance!
I'm just guessing as I've never tried it, but possibly your data tasks are all using the same TCP port on your end.
That would be OK if they were serialized - one after the other, in sequence - but if they overlap, then the server would receive garbled HTTP requests:
GET /foo
GET /bar
GET /baz
What the server would see might be something like:
GET /fGET /baroo
GET /baz
That your third requests actually works OK might be an accident of the timing.
If you absolutely require the three requests to be issued simultaneously, there are ways to open three different ports on your end. I don't know how to do it with Cocoa and Objective-C, but you can certainly do it with C and Berkeley Socket system calls. The Cocoa / Cocoa Touch networking methods are just wrappers around sockets.
A couple of thoughts:
Your technique of using a single NSMutableURLRequest instance, and repeatedly mutating it for each request (while the prior requests are still in progress) is curious.
In the spirit of thread safety, I would use a separate NSMutableURLRequest for each concurrent request. You don't want to risk having your thread issuing these requests mutate the request object while some background thread performing one of the prior requests. (See Apple's Thread Safety Summary in the Threading Programming Guide in which they point out that mutable classes are not generally thread safe.)
Having said that, the NSURLConnection documentation leaves us with the impression that this request object would be copied, mitigating this problem. I don't see this sort of assurance in the NSURLSession documentation (though I suspect it does the same thing).
I don't think this is the problem here (if this was the problem, the problem would likely be more erratic than what you report, and besides, I suspect that NSURLSession is handling this gracefully, anyway), but as a matter of good thread-safe coding habits, it would be prudent to let each concurrent request have its own NSMutableURLRequest object.
You have confirmed that the information being used in the requests looks valid.
If you wanted to take this to the next level, you might use Charles (or Wire Shark or whatever tool you prefer) to observe the actual requests as they go out. These sorts of tools are invaluable for debugging these sorts of problems.
If you observe the requests in Charles and confirm that they are valid, then this categorically eliminates client-side issues from the situation.
What is curious is that you are not receiving NSError object from dataTaskWithRequest. Nor are you receiving statusCode other than 200 from your server. That means that your requests were successfully sent to the server and received by the server.
Instead, the server is processing the request, but is having a problem fulfilling the request. This leads me to wonder about the server code, itself. I suspect that there is something in the server code that is preventing concurrent operations from taking place (e.g., locking some shared resource, such as temp file or SQL table, for the duration of the request). I would take a hard look at the server code and make sure there are no potential contention issues.
Furthermore, I would modify the server code to not simply report "error", but rather to produce a meaningful error message (e.g. system provided error messages, error codes, etc.). Your server is detecting an error, so you should have it tell you precisely what that error was.
Note, I am explicitly not advising you to make your requests run sequentially. That is inadvisable. While it might solve the immediate problem, you pay a huge performance penalty doing that, and it's not scalable. And remember, you really must handle concurrent requests gracefully, as you're likely to have multiple users of the app at some point.
I would take a hard look at the server code, adding further debugging information to the error messages in order to track down the problem.
I put request into for loop, it works. The first thought of rob about NSMutableRequest and NSURLSession seems right, I'm trying to catch the whole idea. Thanks for rob's answer. Anyway, this is code.
for (ImageInformation *temp in self.images.imageInfos) {
// compose request dynamically
NSURL *url = [NSURL URLWithString:#"http://121.199.35.173:8080/xihuan22dcloud/services/Shibietupianservice/serviceGetthetupian"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = #"POST";
NSString *data = [NSString stringWithFormat:#"uid=%#&yangzhengma=%#&tupianid=%#", uid, checkCode, temp.imageId];
request.HTTPBody = [data dataUsingEncoding:NSUTF8StringEncoding];
// data task
dispatch_async(self.serialQueue, ^{
[[self.session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){
// if client side is no errors, continue
if (!error) {
// if server side is no errors, continue
NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response;
if (httpResp.statusCode == 200) {
// in ram and put into images' imageRawData array
[self.images parseImageRawData:[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] withImageId:temp.imageId];
// store data to disk
// [FCFileManager writeFileAtPath:path content:data];
// dispatch display image task to main
dispatch_async(dispatch_get_main_queue(), ^{
if ([self.images.imageDrawDatasDic count] == [self.images.imageInfos count]) {
[self.tableView reloadData];
}
});
}
}
}] resume];
[NSThread sleepForTimeInterval:0.5];
});
}
}
I'm using synchronous requests for the first time and would love some help. (The code I'm writing is solely for my own use, and given its purposes synchronous requests are not a problem.)
The code gets data from a web page in a series, manipulates the data, moves on to the next page in the series, manipulates THAT data, and so on. I'm using a synchronous request because I need the connection to finish loading and the data to be manipulated before the function loops to the next page.
Here's my looping code:
-(NSData *)myMethod {
NSString *string;
NSData *data;
for (int x = 1; x<100; x++) {
string = [NSString stringWithFormat:#"http://www.blahblah.com/%d",(x)];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:string]];
NSURLResponse *response = nil;
NSError *error = nil;
data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
}
return data;
}
When I was using connectionWithRequest, I just put the code to manipulate the data in connectionDidFinishLoading and it worked fine. But with sendSynchronousRequest, even though NSLog shows that the loop code is looping, the code in connectionDidFinishLoading never runs.
How can I fix this?
(Or am I taking the wrong approach completely?)
Here's how to take #nhgrif's good advice to perform asynch and preserve all of the results.
- (void)doRequest:(NSInteger)requestIndex gatheringResultsIn:(NSMutableArray *)array completion:(void (^)(void))completion {
if (requestIndex < 100) {
NSString *string = [NSString stringWithFormat:#"http://www.blahblah.com/%d",(requestIndex)];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:string]];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (data) [array addObject:data];
[self doRequest:requestIndex+1 gatheringResultsIn:array completion: completion];
}];
} else {
completion();
}
}
This will run 100 requests indexed 0..99 placing the results in a mutable array. Call it like this:
NSMutableArray *results = [NSMutableArray array];
[self doRequest:0 gatheringResultsIn:results completion:^{
NSLog(#"100 NSData objects should be here: %#", results);
}];
connectionDidFinishLoading is an NSURLConnection delegate method for when you've sent asynchronous requests. Normally, you'd implement this method to get the data that loaded, but you don't need to do this, as it's returned synchronously and assigned to your data variable.
I will note however, you are definitely taking a poor approach here.
First of all, if you'd use asynchronous requests here, you could query all 100 URLs as basically the same time and let them return in their own time.
But what's more problematic is what actually happens with your code.
We create a URL, send the synchronous request, and when it finishes, assign the return to data.
... then we loop. And do this 99 times. 99 times we make this synchronous request (to a different URL each time) and overwrite the data that the previous request loaded. And after the 100th time, we exit the loop and return the data we downloaded in the final request.
I'm working with an app that requests data from an OAuth2.0 protected server. When I use the GTM OAuth Library to retrieve data, the program continues to run while the data is being downloaded in the background. I need some sort of mechanism to either force my application to wait until the didFinishWithData selector is called,or I need a way to notify my ViewController of the download's completion, so I can then utilize the data immediately.
I've tried conditional blocks, but those aren't doing it for me. I've also tried polling the object whose data I'm interested in, but if I do that, the data never seems to download. I've heard I can somehow utilize the Notification Center to accomplish this task, so I'll look more into that while I'm waiting for replies here.
Here is basically what is going on:
-(void) getAlert{
// Define the URL of the API module we'd like to utilize.
NSURL *url = [[NSURL alloc] initWithString:#"https://access.active911.com/interface/open_api/api/alerts"];
// Constructs a an HTTP request object to send to the server in order to obtain data.
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setValue:#"1" forHTTPHeaderField:#"alert_days"];
// This fetcher sends the request along with the authentication header in a recognizable manner.
GTMHTTPFetcher *fetcher = [[GTMHTTPFetcher alloc] initWithRequest:request];
// Attach the OAuth credentials for the fetcher's use.
[fetcher setAuthorizer:auth];
// Execute the operation.
[fetcher waitForCompletionWithTimeout:10];
NSLog(#"About to get alert");
[fetcher beginFetchWithDelegate:self didFinishSelector:#selector(responseHandler:finishedWithData:finishedWithError:)];
NSLog(#"got alert");
}
-(void)responseHandler:(id)valueNotUsed finishedWithData:(NSData *)data finishedWithError:(NSError *)error{
// Retrieve the server data in a usable object
// All that's being done here is conversion to an NSDictionary
// followed by the creation of subdictionaries from that dictionary
// until our final value can be picked directly out of the resulting dict
NSData *jsonData = [[NSData alloc] initWithData:data];
NSError *dictError;
NSDictionary* json = [NSJSONSerialization
JSONObjectWithData:jsonData //1
options:kNilOptions
error:&dictError];
NSDictionary *token = [json objectForKeyedSubscript:#"message"];
NSArray *alerts = [token objectForKeyedSubscript:#"alerts"];
NSDictionary *alertData = alerts[0];
mapCode = [alertData objectForKeyedSubscript:#"map_code"];
NSString *city = [alertData objectForKeyedSubscript:#"city"];
NSLog(#"Map code: '%#' with city '%#' and access token %#", mapCode, city, accessToken);
}
And I need to pass the mapCode to my view controller.
Thanks for the help!
First off, please rethink about having the UI halt while you fetch results from the server. This can create an extremely bad UX for the app and only should be done if absolutely necessary.
Second, does your responseHandler method work? And do you only need mapCode in the VC that responseHandler is in?
If so, you don't even need to use Notifications. Simply do:
-(void)responseHandler:(id)valueNotUsed finishedWithData:(NSData *)data finishedWithError:(NSError *)error{
...
...
mapCode = [alertData objectForKeyedSubscript:#"map_code"];
[self updateVCWithMapCode:mapCode];
}
That will call the method after the response has been received. Passing it explicitly too so you don't need to have mapCode be a property as well.