I'm trying to make an http DELETE request using NSURLSession, but it's not completely working. The server deletes the resource, but the NSURLSession method dataTaskWithRequest: completionHandler: returns a time out error after waiting for the specified timeout.
I am not using NSURLConnection because it is deprecated.
Of the NSURLSession methods to use, I chose dataTaskWithRequest because it is most similar to the method I use for http GET: dataTaskWithUrl: completionHandler. The methods beginning with "uploadTask" and "downloadTask" don't seem appropriate for a DELETE, but downloadTaskWithRequest: completionHandler: 'worked' in the same way as the dataTask method above. The server deleted the resource, but the method returned a time out error.
Here is the code:
+(void)httpDelete: (NSString*)url completionHandler: (void(^)(id, NSError*))complete
{
NSURLSessionConfiguration *urlSessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSMutableDictionary* dictionaryAdditionalHeaders = [[NSMutableDictionary alloc] init];
NSString* stringBearerToken = #"...";
NSString* stringApiKey = #"...";
[dictionaryAdditionalHeaders setObject:stringBearerToken forKey:#"Authorization"];
[dictionaryAdditionalHeaders setObject:stringApiKey forKey:#"x-api-key"];
[dictionaryAdditionalHeaders setObject:#"application/json" forKey:#"Content-Type"];
[dictionaryAdditionalHeaders setObject:#0 forKey:#"Content-Length"];
[urlSessionConfiguration setHTTPAdditionalHeaders: dictionaryAdditionalHeaders];
NSURLSession *urlSession = [NSURLSession sessionWithConfiguration: urlSessionConfiguration delegate:nil delegateQueue:[NSOperationQueue mainQueue]];
NSMutableURLRequest* mutableUrlRequest = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:url] cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:5];
[mutableUrlRequest setHTTPMethod: #"DELETE"];
[[urlSession dataTaskWithRequest:mutableUrlRequest completionHandler: ^(NSData *data, NSURLResponse* response, NSError* error)
{
if(error != nil)
{
complete(response, error);
}
else
{
complete(response, nil);
}
}] resume];
}
Using Postman, the DELETE call returns with a 204 immediately.
Am I using NSURLSession correctly for a delete request?
It turns out the Amazon API Gateway incorrectly sends a Content-Length header with a 204 response. They added the issue to their backlog March 21, 2016 according to this AWS forum. When I increased the timeout interval of the NSMutableURLRequest to a ridiculous 300 seconds, the dataTaskWithRequest method returns with a real response instead of timing out.
This isn't an error with NSURLSession - it means that your request is actually timing out. That means that there's an error on the back-end (maybe it's not reaching your server at all?)
Also, I've found these issues much easier to debug using a third-party framework to send my HTTP requests. AFNetworking is a really good one.
Related
I try to download a zip-archive using NSURLSessionDataTask.
I am aware that there is a NSURLSessionDownloadTask, but the point is I want a didReceiveData callback (to show the progress).
The code is:
NSURLRequest *request = [NSURLRequest requestWithURL:#"..."
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSOperationQueue *myQueue = [NSOperationQueue new];
myQueue.underlyingQueue = dispatch_get_main_queue();
NSURLSession *session = [NSURLSession sessionWithConfiguration:config
delegate:self
delegateQueue:myQueue];
NSURLSessionDataTask* task = [session dataTaskWithRequest:request
completionHandler:^( NSData *data, NSURLResponse *response, NSError *error){ ... }
[task resume];
My class conforms to NSURLSessionDataDelegate.
When I call the method, after several seconds debugger goes to completionHandler with nil data and nil error.
What am I doing wrong?
I also tried:
calling without completionHandler, then debugger goes to didReceiveResponse callback with 200 response and that's all.
using [NSOperationQueue new] for the queue
using [NSURLSession sharedSession] - didn't get any response
using [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier: #"..."] - falls saying that I can't use a completionHandler, but without it - also no response.
So I have found the answer and it's not quite obvious from documentation:
I had several callbacks, and among them didReceiveResponse.
Turns out I have to call completion handler in order for the future callbacks to work, i.e:
completionHandler(NSURLSessionResponseAllow);
And one more thing: didCompleteWithError is actually the delegate that tells about successful finish, too, although the name implies that this is the error handler.
What it means: when a download is successfully finished, this function is called with error = nil.
Hope this will be useful for somebody someday.
I have a very strange problem, in Android, the API calls take 300-500 ms while in iOS it takes 1.5-2.5 sec. I have removed dependencies like my server, device specific issue, internet connectivity etc. I have a very simple sample code hitting a sample URL and for me, it takes about 2 sec, even on the simulator.
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(#"start");
// 1
NSURL *url = [NSURL URLWithString:#"https://httpbin.org/get"];
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
// 2
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setValue:#"" forHTTPHeaderField:#"Accept-Encoding"];
request.HTTPMethod = #"GET";
[[session dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// Handle response here
NSLog(#"end - %ld", (long)[(NSHTTPURLResponse*)response statusCode]);
}] resume];
});
I also have tried using AFNetworking and ASSIHTTP libraries, but there is no difference. I also have checked the headers and they are the same in both Android and iOS. What am I doing something wrong here?
I think your problem is not in the network but in this line:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
Can you remove it and check again?
Data task is async so you don't need to wrap it in another async block.
Also you don't need to create instance of NSURLSession for every request
You can log out the timestamp (NSLog already did) when the request generate, send, callback to analyse it.
Does the server side code do any processing based on 'user-agent'?
Is there a time difference if you open the url in iOS safari and within the app ?
You can try calling the api from postman (or another REST API test tool like firefox RESTClient) and override the user-agent to use iOS values (http://www.enterpriseios.com/wiki/UserAgent). If the time difference is still the same, theres nothing you can do in your mobile code to fix this lag.
P.S. :
1. Overriding user-agent in postman needs some tweaking : https://github.com/postmanlabs/postman-app-support/wiki/Postman-Proxy
I have implemented a program to communicate http2 using NSURLSession of iOS9, And it can communicate with my server in http2.
However, I'm having a problem with receive server_push.
I found ENABLE_PUSH value is 0 in their settings and there's no delegate in receive server push in NSURLSession...
・I think NSURLSession doesn't support server_push. Is this right?
・If it support server_push,how to use?
/**
It WORKS for post data and get response.
I don't know the code should be added here
in order to get the server_push.
I suspect that NSURLSession itself cannot receive the server_push(;_;)
**/
- (void) postData
{
NSString *urlstr = self.urlArea.text;
NSURL * url = [NSURL URLWithString:urlstr];
NSDictionary *params = #{#"data":#""};
//json to query
NSData *query = [self buildQueryWithDictionary: params];
//make request
NSMutableURLRequest *request = [NSMutableURLRequest
requestWithURL:url
cachePolicy: NSURLRequestReloadIgnoringCacheData
timeoutInterval: 10.0];
[request setHTTPMethod: #"POST"];
[request setHTTPBody: query];
//prepare session
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
//resume
[[session dataTaskWithRequest: request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
{
if (response && ! error) {
NSLog(#"Data: %#", [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding]);
}else {
NSLog(#"ERR: %#", error);
}
}] resume];
}
I read this here:
The HTTP2 push mechanism is not a generic server push mechanism like
websocket or server sent events.
It is designed for a specific optimisation of HTTP conversations.
Specifically when a client asks for a resource (eg index.html) the
server can guess that it is going to next ask for a bunch of
associated resources (eg theme.css, jquery.js, logo.png, etc. etc.)
Typically a webpage can have 10s of such associated requests.
With HTTP/1.1, the server had to wait until the client actually sends
request for these associated resources, and then the client is limited
by connections to only ask for approx 6 at a time. Thus it can take
many round trips before all the associated resources that are needed
by a webpage are actually sent.
With HTTP/2, the server can send in the response to the index.html GET
push promises to tell the client that it is going to also send
theme.css, jquery.js, logo.png, etc. as if the client had requested
them. The client can then cancel those pushes or just wait for them to
be sent without incurring the extra latency of multiple round trips.
ere is a blog about the push API for HTTP2 and SPDY in jetty:
https://webtide.com/http2-push-with-experimental-servlet-api/
Solved
I received following reply from support.
iOS does not currently support this.
update
(#vin25 comment)
ios10 supports it.
I created an NSURLSessionConfiguration with some default settings but when I see the request object made with that configuration in my custom NSURLProtocol it doesn't seem that all those settings are inherited and I'm a bit confused.
NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSMutableArray *protocolsArray = [NSMutableArray arrayWithArray:config.protocolClasses];
[protocolsArray insertObject:[CustomProtocol class] atIndex:0];
config.protocolClasses = protocolsArray;
// ex. set some random parameters
[config setHTTPAdditionalHeaders:#{#"Authorization":#"1234"}];
[config setAllowsCellularAccess:NO];
[config setRequestCachePolicy:NSURLRequestReturnCacheDataElseLoad];
[config setHTTPShouldSetCookies:NO];
[config setNetworkServiceType:NSURLNetworkServiceTypeVoice];
[config setTimeoutIntervalForRequest:4321];
// Create a request with this configuration and start a task
NSURLSession* session = [NSURLSession sessionWithConfiguration:config];
NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"https://google.com"]];
NSURLSessionDataTask* task = [session dataTaskWithRequest:request];
[task resume];
In my custom NSURLProtocol that is registered
- (void)startLoading {
...
// po [self.request valueForHTTPHeaderField:#"Authorization"] returns 1234
//
// However, I'm very confused why
//
// - allowsCellularAccess
// - cachePolicy
// - HTTPShouldHandleCookies
// - networkServiceType
// - timeoutInterval
//
// for the request return the default values unlike for the header
...
}
Is there some way to check that those parameters I've set are obeyed and inherited by the request?
When dealing with http requests, it is helpful to start with the basics, such as is the request actually being made by the OS, and is a response being received? This will in part help to answer your question about checking that set parameters are infact being obeyed by the request.
I would challenge your use of the word "inherit" in the phrase
Is there some way to check that those parameters I've set are obeyed and inherited by the request?
Inheritance in Object Oriented programming has a very specific meaning. Did you in fact create a custom subclass (let's call it SubClassA) of NSURLRequest with specific properties, and then a further subclass (let's call it SubClassB), and are expecting the second subclass (SubClassB) to inherit properties from its parent (SubClassA)? If so, this is certainly not indicated in the code you provided.
There are several HTTP Proxy programs available which help confirm whether or not the HTTP request is being sent, if a response is received, and also which allow you to inspect the details of the request and the response. Charles HTTP Proxy is one such program. Using Charles, I was able to determine that your code as provided is not making any HTTP request. So you cannot confirm or deny any parameters if the request is not being made.
By commenting out the lines including the CustomProtocol as part of the NSURLSession configuration, and running your code either with or without these lines, I gained some potentially valuable information:
by commenting out the lines including the CustomProtocol, a request was in fact made (and failed), as informed by Charles HTTP Proxy. I also added a completion block to your method dataTaskWithRequest. This completion block is hit when the CustomProtocol configuration lines are commented out. The CustomProtocol's startLoading method is not hit.
when leaving in the original lines to configure the NSURLSession using the CustomProtocol, there was no request recorded by Charles HTTP Proxy, and the completion handler is not hit. However, the CustomProtocol's startLoading method is hit.
Please see code below (modifications made to the code posted in the original question).
NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSMutableArray *protocolsArray = [NSMutableArray arrayWithArray:config.protocolClasses];
//[protocolsArray insertObject:[CustomProtocol class] atIndex:0];
//config.protocolClasses = protocolsArray;
// ex. set some random parameters
[config setHTTPAdditionalHeaders:#{#"Authorization":#"1234"}];
[config setAllowsCellularAccess:NO];
[config setRequestCachePolicy:NSURLRequestReturnCacheDataElseLoad];
[config setHTTPShouldSetCookies:NO];
[config setNetworkServiceType:NSURLNetworkServiceTypeVoice];
[config setTimeoutIntervalForRequest:4321];
// Create a request with this configuration and start a task
NSURLSession* session = [NSURLSession sessionWithConfiguration:config];
NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"https://google.com"]];
NSURLSessionDataTask* task = [session dataTaskWithRequest:request
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSString * auth = [request valueForHTTPHeaderField:#"Authorization"];
NSLog(#"Authorization: %#", auth);
BOOL allowsCellular = [request allowsCellularAccess];
NSString * allowsCellularString = allowsCellular ? #"YES" : #"NO";
NSLog(#"Allows cellular: %#", allowsCellularString);
}];
[task resume];
This gives you the information that the CustomProtocol is not properly handling the request. Yes, the breakpoint inside the startLoading method is hit when the CustomProtocol is configured as part of the NSURLSession, but that is not definitive proof that the CustomProtocol is handling the request properly. There are many steps necessary to using a CustomProtocol, as outlined by Apple (Protocol Support, NSURLProtocol Class Reference) that you should confirm you are following.
Some things to make sure are working:
if you are using a CustomProtocol, that means you are likely trying to handle a different protocol other than http, https, ftp, ftps, etc.
make sure that your end point (the server which is listening for the http requests and responding) can actually accept the request and reply.
if you are setting an HTTP Authorization Header, make sure that the server can respond appropriately, and that the credentials are valid if you are expecting a positive response
remember to register your CustomProtocol
for example:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
[NSURLProtocol registerClass:[CustomProtocol class]];
return YES;
}
Below is a unit tests to verify that the NSURLSession is functioning as expected (without using our custom protocol explicitly). Note that this unit test does pass when added to Apple's own sample code for the project CustomHTTPProtocol, but does not pass using our very bare bones CustomProtocol
- (void)testNSURLSession {
XCTestExpectation *expectation = [self expectationWithDescription:#"Testing standard NSURL Session"];
[[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:#"https://www.apple.com/"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
#pragma unused(data)
XCTAssertNil(error, #"NSURLSession test failed with error: %#", error);
if (error == nil) {
NSLog(#"success:%zd / %#", (ssize_t) [(NSHTTPURLResponse *) response statusCode], [response URL]);
[expectation fulfill];
}
}] resume];
[self waitForExpectationsWithTimeout:3.0 handler:^(NSError * _Nullable error) {
if(nil != error) {
XCTFail(#"NSURLSession test failed with error: %#", error);
}
}];
}
Below is a unit test which may be used to verify that the configurations made to a NSURLSession are as expected, when configuring using our own CustomProtocol class. Again, please note that this test fails using the empty implementation of CustomProtocol but this is expected if using Test Driven Development (create the test first, and then the code second which will allow the test to pass).
- (void)testCustomProtocol {
XCTestExpectation *expectation = [self expectationWithDescription:#"Testing Custom Protocol"];
NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSMutableArray *protocolsArray = [NSMutableArray arrayWithArray:config.protocolClasses];
[protocolsArray insertObject:[CustomProtocol class] atIndex:0];
config.protocolClasses = protocolsArray;
// ex. set some random parameters
[config setHTTPAdditionalHeaders:#{#"Authorization":#"1234"}];
[config setAllowsCellularAccess:NO];
[config setRequestCachePolicy:NSURLRequestReturnCacheDataElseLoad];
[config setHTTPShouldSetCookies:NO];
[config setNetworkServiceType:NSURLNetworkServiceTypeVoice];
[config setTimeoutIntervalForRequest:4321];
// Create a request with this configuration and start a task
NSURLSession* session = [NSURLSession sessionWithConfiguration:config];
NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"https://www.apple.com"]];
NSURLSessionDataTask* task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
#pragma unused(data)
XCTAssertNil(error, #"test failed: %#", error.description);
if (error == nil) {
NSLog(#"success:%zd / %#", (ssize_t) [(NSHTTPURLResponse *) response statusCode], [response URL]);
NSString * auth = [request valueForHTTPHeaderField:#"Authorization"];
NSLog(#"Authorization: %#", auth);
XCTAssertNotNil(auth);
BOOL allowsCellular = [request allowsCellularAccess];
XCTAssertTrue(allowsCellular);
XCTAssertEqual([request cachePolicy], NSURLRequestReturnCacheDataElseLoad);
BOOL shouldSetCookies = [request HTTPShouldHandleCookies];
XCTAssertTrue(shouldSetCookies);
XCTAssertEqual([request networkServiceType], NSURLNetworkServiceTypeVoice);
NSTimeInterval timeOutInterval = [request timeoutInterval];
XCTAssertEqualWithAccuracy(timeOutInterval, 4321, 0.01);
[expectation fulfill];
}
}];
[task resume];
[self waitForExpectationsWithTimeout:3.0 handler:^(NSError * _Nullable error) {
if(nil != error) {
XCTFail(#"Custom Protocol test failed with error: %#", error);
}
}];
}
In my current project I am using, +(void)postJSONFromURLWithString:(NSString*)urlString params:(NSDictionary*)params completion:(JSONObjectBlock)completeBlock; method to create account and log in my application for the very first time . For second time and onwards, log in call is not there, application directly opens the user's profile screen. But when I am updating user profile (name, contact number etc.), I am getting response status code 403 from by the statement
NSLog(#"Response status code = %i", (int)response.statusCode);
added in implementation of method
+(NSData*)syncRequestDataFromURL:(NSURL*)url method:(NSString*)method requestBody:(NSData*)bodyData headers:(NSDictionary*)headers etag:(NSString**)etag error:(JSONModelError**)err
403 is generally invoked due to authorization failure in server side.
Is there any way to see what are the cookies are going to server side while I am making an API call with
+(void)postJSONFromURLWithString:(NSString*)urlString params:(NSDictionary*)params completion:(JSONObjectBlock)completeBlock;?
JSONModel's built-in networking support is intentionally very basic/primitive/limited. It does not support custom authentication, cookies, etc.
You'd be best making a request with NSURLSession to get the data, then use JSONModel just for the deserialization - rather than for the whole request.
e.g.:
NSURLSession *session = [NSURLSession sharedSession];
NSURL *url = [NSURL URLWithString:#"https://example.com/mydata.json"];
NSURLSessionDataTask *task = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
MyModel *obj = [[MyModel alloc] initWithData:data error:nil];
}];
[task resume];