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.
Related
I have a method which starts an NSURLConnetion to read an IP-Address an return the IP after the connection did finish loading:
- (NSString *)getHansIP
{
self.returnData = [[NSMutableData alloc] init];
NSMutableURLRequest *getIpRequest = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:#"https://example.com"]];
[getIpRequest setHTTPMethod:#"GET"];
NSURLConnection *ipconn = [NSURLConnection connectionWithRequest:getIpRequest delegate:self];
[ipconn start];
return self.ipString;
}
The problem is that objective-c tries to return the IP-Address (ipString) when the connection did not finished loading yet. I know there is a simple way to fix it but with this way the NSURLConnectionDelegate methods do not getting executed and I need thedidReceiveAuthenticationChallenge Method an the canAuthenticateAgainstProtectionSpace method.
P.S. hope you understand my bad school english :P
You can't return the IP direct from the method, not without blocking the thread anyway, and you can't really do that and handle auth issues.
You need to create an object to manage this which is the delegate for the connection, maintains the connection as an instance variable while it's processing and has a delegate or a completion callback block to pass the IP back with once it's done.
You need to embrace the fact that the connection is asynchronous and not want to return the IP from the method directly.
I've looked around a lot and cant seem to find a proper answer for my problem. As of now I have a network engine and I delegate into that from each of the view controllers to perform my network activity.
For example, to get user details I have a method like this:
- (void) getUserDetailsWithUserId:(NSString*) userId
{
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:#"%#Details", kServerAddress]]];
request.HTTPMethod = #"POST";
NSString *stringData = [NSString stringWithFormat:#"%#%#", kUserId, userId];
NSData *requestBodyData = [stringData dataUsingEncoding:NSUTF8StringEncoding];
request.HTTPBody = requestBodyData;
NSURLConnection *conn = [[NSURLConnection alloc] init];
[conn setTag:kGetUserInfoConnection];
(void)[conn initWithRequest:request delegate:self];
}
And when I get the data in connectionDidFinishLoading, I receive the data in a NSDictionary and based on the tag I've set for the connection, I transfer the data to the required NSDictionary.
This is working fine. But now I require two requests going from the same view controller. So when I do this, the data is getting mixed up. Say I have a connection for search being implemented, the data from the user details may come in when I do a search. The data is not being assigned to the right NSDictionary based on the switch I'm doing inside connectionDidFinishLoading. I'm using a single delegate for the entire network engine.
I'm new to NSURLConnection, should I setup a queue or something? Please help.
EDIT
Here's the part where I receive data in the connectionDidFinishLoading:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
if ([connection.tag integerValue] == kGetUserDetails)
networkDataSource.userData = self.jsonDetails;
if ([connection.tag integerValue] == kSearchConnection)
networkDataSource.searchData = self.jsonDetails;
}
and after this I have a switch case that calls the required delegate for the required view controller.
Anil here you need to identify for which request you got the data,
simplest way to check it is as below,
- (void)connectionDidFinishLoading:(NSURLConnection *)conn
{
// Check URL value for request, url for search and user details will be different, put if condition as per your need.
conn.currentRequest.URL
}
Try using conn.originalRequest.URL it will give original request.
You can do in many ways to accomplish your task as mentioned by others and it will solve your problem . But if you have many more connections , you need to change your approach.
You can cretae a subclass of NSOperation class. Provide all the required data, like url or any other any informmation you want to get back when task get accomplish , by passing a dictionary or data model to that class.
In Nsoperation class ovewrite 'main' method and start connection in that method ie put your all NSURRequest statements in that method. send a call back when download finish along with that info dict.
Points to be keep in mind: Create separte instance of thet operation class for evey download, and call its 'start method'.
It will look something like :
[self setDownloadOperationObj:[[DownloadFileOperation alloc] initWithData:metadataDict]];
[_downloadOperationObj setDelegate:self];
[_downloadOperationObj setSelectorForUpdateComplete:#selector(callBackForDownloadComplete)];
[_downloadOperationObj setQueuePriority:NSOperationQueuePriorityVeryHigh];
[_downloadOperationObj start];
metaDict will contain your user info.
In DownloadFileOperation class you will overwrite 'main' method like :
- (void)main {
// a lengthy operation
#autoreleasepool
{
if(self.isCancelled)
return;
// //You url connection code
}
}
You can add that operation to a NSOperationQueue if you want. You just need to add the operation to NSOperationQueue and it will call its start method.
Declare two NSURLConnection variables in the .h file.
NSURLConnection *conn1;
NSURLConnection *conn2;
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
if(connection == conn1){
}
else if(connection == conn2){
}
}
I've implemented a class where I encapsulate an asynchronous request with NSURLConnection and its delegate methods stuff. I create an instance of this class in a view controller whenever a button of its view is tapped, and I ask it for making the network request:
- (IBAction)getData:(id)sender
{
// Perform network request
Updater *updater = [[Updater alloc] init];
[updater queryService:self.date];
}
Such queryService: method is like this:
- (void)queryService:(NSDate *)date
{
self.responseData = [NSMutableData data];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:kTimeout];
NSURLConnection *connection = [NSURLConnection connectionWithRequest:request delegate:self];
}
Since Updater is performing an asynchronous operation, I'm not sure if the updater instance, which I've declared as local variable, will be retained until connection:didFailWithError: or connectionDidFinishLoading: are called, or I'd create a strong property for the Updater in the calling view controller. I'm using ARC.
Thanks!
Yes, it will be retained until the connection ends (fail, success ecc).
It happens because your Updater instance is the delegate of NSURLConnection.
Inside NSURLConnection doc you can read:
Note: During a download the connection maintains a strong reference to the delegate. It releases that strong reference when the connection finishes loading, fails, or is canceled.
You need a strong reference, could be' an ivar to Update or an ivar to a collection with update instances. Even if the delegate is retained you did not explain how you get data back.
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.
I am using a subclass of NSURLProtocol to intercept all HTTP calls and modify the user agent as well as add a other http headers required by my server.
-(id)initWithRequest:(NSURLRequest *)request
cachedResponse:(NSCachedURLResponse *)cachedResponse
client:(id <NSURLProtocolClient>)client
{
NSMutableURLRequest* lInnerRequest;
//************************************************
lInnerRequest = [request mutableCopy];
[lInnerRequest setValue:#"MyUserAgent" forHTTPHeaderField:#"User-Agent"];
//************************************************
self = [super initWithRequest:lInnerRequest
cachedResponse:cachedResponse
client:client];
//************************************************
if (self)
{
self.innerRequest = lInnerRequest;
}
//***********************************00*************
[lInnerRequest release];
//************************************************
return self;
}
My protocol then uses an NSURLConnection
- (void)startLoading
{
self.URLConnection = [NSURLConnection connectionWithRequest:self.innerRequest delegate:self];
}
I then implement all the delegate method in the NSURLConnection by forwarding the call to the equivalent NSURLProtocolClient method.
This works well in general but when I am uploading data to the server, my code which is using NSURLConnection does not get called back on:
connection:didSendBodyData:totalBytesWritten:totalBytesExpectedToWrite:
I understand why this is since I didn't implement that method in the NSURLProtocol as there are no equivalent NSURLProtocolClient method which can be used to report upload progress.
Has someone found any workaround for this?
add this line to your code to make it a HTTP POST request:
[lInnerRequest setHTTPMethod:#"POST"];
Use setProperty:forKey:inRequest: to save NSURLConnection and delegate object, then use propertyForKey:inRequest: to load NSURLConnection object and delegate object in custom NSURLProtocol class, when data sent, use the NSURLConnection object and delegate to call the delegate method