Sending chunked HTTP 1.1 requests in Objective-C - ios

I have the following issue: I am creating a very big SOAP request (the data is a video encoded as Base64 string) and because of that I cannot send it as a raw SOAP request but rather need to send it in HTTP 1.1 chunks. I cannot seem to figure out how to do it. I used the code in here:
What are alternatives to NSURLConnection for chunked transfer encoding
but it doesn't seem to be doing what I think it should - I can see that the request arrives on the server as a single request instead of many chunks (I am using WireShark on the server to see the incoming traffic.)
I know that a similar functionality on an Android works using Apache Foundations HTTP libraries for Java - with these, any HTTP request whose length is not specified in advance is transmitted as an HTTP 1.1 Chunked Request - and I can see indeed those requests arriving on the server as individual chunks... I want to emulate that.
(UPDATE: Seems to me AFNetworking might have the functionality, but I fail to find any example as to how to use it.)
Here is my code, more or less:
NSString *soapBody = ....; //some correctly formed SOAP request XML here
NSURL *url = [NSURL URLWithString:...];
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
[request addValue: ... forHTTPHeaderField:#"SOAPAction"];
[request setHTTPMethod:#"POST"];
[request addValue:#"text/xml" forHTTPHeaderField:#"Content-Type"];
[request setHTTPBody:[soapBody dataUsingEncoding:NSUTF8StringEncoding]];
ChunkedTransferConnection* connection = [ChunkedTransferConnection alloc];
[connection establishConnectionWithRequest:request];
where ChunkedTransferConnection implementation is the following
#implementation ChunkedTransferConnection
#synthesize p_connection;
#synthesize p_responseData;
- (void)establishConnectionWithRequest:(NSMutableURLRequest *)request
{
self.p_responseData = [[NSMutableData alloc] initWithLength:0] ;
self.p_connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES];
}
...
#end

Figured it out:
NSInputStream *dataStream = [NSInputStream inputStreamWithData:[soapBody dataUsingEncoding:NSUTF8StringEncoding]];
[request setHTTPBodyStream:dataStream];
This causes the request automatically be in HTP 1.1 chunks!

Related

How to send HTTP body plain text for GET method?

I had a problem, I use the iGDB REST API which need to send some plain text for some endpoints with GET method.
There is no problem with PostMan (by selecting "Body" > "raw" & paste my query), but when I try with Objective-C, an error appear telling me "GET method must not have a body"...
Here is the code used in my app:
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:#"MY_URL"]];
[request addValue:#"text/plain" forHTTPHeaderField:#"Content-Type"];
[request setHTTPMethod:#"GET"];
[request setHTTPBody:[#"sort popularity desc;" dataUsingEncoding:NSUTF8StringEncoding]];
EDIT 02/10/2019
Trying to add each filters in headers not working...
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:#"MY_URL"]];
[request setValue:[[#"id,name,first_release_date,release_dates,cover,platforms" dataUsingEncoding:NSUTF8StringEncoding] base64EncodedStringWithOptions:0] forHTTPHeaderField:#"fields"];
[request setValue:[[#"popularity desc" dataUsingEncoding:NSUTF8StringEncoding] base64EncodedStringWithOptions:0] forHTTPHeaderField:#"sort"];
[request setValue:[[#"5" dataUsingEncoding:NSUTF8StringEncoding] base64EncodedStringWithOptions:0] forHTTPHeaderField:#"limit"];
[request setValue:[[#"0" dataUsingEncoding:NSUTF8StringEncoding] base64EncodedStringWithOptions:0] forHTTPHeaderField:#"offset"];
Thanks in advance for any replies !
It's no longer possible to send a GET request with a body, you'll have to send the data via the query string of the URL, either by building the string manually or with the help of NS(Mutable)URLComponents.
As per the iOS 13 release notes, GET requests are no longer allowed to have a body:
All URLSessionTask instances with a GET HTTP method that contain a body now produce the error NSURLErrorDataLengthExceedsMaximum. (46025234)
This makes URLSession more conformant with the HTTP/1.1 RFC:
A message-body MUST NOT be included in
a request if the specification of the request method (section 5.1.1)
does not allow sending an entity-body in requests.
If I get true your question, you can set parameters to your request header when you get you can set like below.
[request addValue:#"valueForKey1" forHTTPHeaderField:#"key1"];
[request addValue:#"valueForKey2" forHTTPHeaderField:#"key2"];
If you send a data format so,
NSString *stringValueOfParameters =[NSString stringWithFormat:#"userName:blabla"];
NSData *convertedDat=[stringValueOfParameters dataUsingEncoding:NSUTF8StringEncoding];
NSString *headerValue=[NSString stringWithFormat:#"Basic %#",[convertedDat base64EncodedStringWithOptions:0]];
[request setValue:headerValue forHTTPHeaderField:#"headerKey"];
// Edit
You must try kinda like that, directly write a Dictionary or like below write your JSON parameters as NSString then convert it to Data.
NSDictionary *dictParams = #{ #"fields" : #[#"id",#"name",#"first_release_date",#"release_dates",#"cover",#"platforms"], #"sort" : #"popularity desc", #"limit": #5, #"offset":#0 };
// this is taken from an example
NSString *jsonString = #"{\"ID\":{\"Content\":268,\"type\":\"text\"},\"ContractTemplateID\":{\"Content\":65,\"type\":\"text\"}}";
NSData *data = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
Then try it to set header.

Using NSURLSession to POST, what is the correct way to post the variables?

I am following this tutorial: http://www.raywenderlich.com/2965/how-to-write-an-ios-app-that-uses-a-web-service. Trying to set up a basic web service. Seems like the tutorial is old material and ASIHTTPRequest is no longer continued. I have been trying to use NSURLRequest instead. First question, is NSURLRequest a pretty standard way to be doing this? I just want something for basic GET, POST etc, should I be doing it a different way?
My code is:
-(BOOL)textFieldShouldReturn:(UITextField *)textField{
NSLog(#"We want to unlock for the code %#",self.textField.text);
//Get a device ID, (actually can't do this aymore)
NSString *uniqueIdentifier = #"My iPhone";
NSString *code = self.textField.text;
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:#"http://www.madasd.co/promos/"]];
request.HTTPMethod=#"POST";
//Set the header fields
[request setValue:#"application/xml; charset=utf-8" forHTTPHeaderField:#"Content-Type"];
NSString *myString = [NSString stringWithFormat:#"rw_app_id=1&code=%#&device_id=%#",code,uniqueIdentifier];
NSLog(#"%#",myString);
NSData *requestBodyData = [myString dataUsingEncoding:NSUTF8StringEncoding];
request.HTTPBody=requestBodyData;
//Create url and fire request
NSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[conn start];
return TRUE;
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"%#",string);
}
Second question, I have tested the backend using curl so I know it works fine, however the response I get is "Invalid Request", I think this is because the string I am sending is not correct. Am I doing this correct using the var names and & operators? Any pointers on this would be great! thanks. (Running a LAMP server on Linode!)
EDIT:
Also tried sending as JSON:
[request addValue:#"application/json" forHTTPHeaderField:#"Content-Type"];
[request addValue:#"application/json" forHTTPHeaderField:#"Accept"];
NSDictionary *mapData = [[NSDictionary alloc]initWithObjectsAndKeys:#"1",#"rw_app_id",code,#"code",uniqueIdentifier,#"device_id", nil];
NSError *error = nil;
NSData *requestBodyData = [NSJSONSerialization dataWithJSONObject:mapData options:0 error:&error];
request.HTTPBody=requestBodyData;
Still getting the same error.
A couple of thoughts:
Don't use NSURLConnection. It is deprecated as of iOS 9. Use NSURLSession. See Using NSURLSession in the URL Loading System Programming Guide.
Decide what type of request you need to prepare. You specified application/xml in your header, but are creating a application/x-www-form-urlencoded request. Your Content-Type header must match how you're building the HTTPBody.
What type of request does your server require? x-www-form-urlencoded? XML? JSON?
Also, what type of response does your server provide?
If building a application/x-www-form-urlencoded request (as suggested by the body of your request), you are not properly percent escaping the values (see https://stackoverflow.com/a/20398755/1271826).
If you use delegate based NSURLConnection or NSURLSession, you should not just grab the results in didReceiveData. What you need to do is
Instantiate a NSMutableData before starting the request;
Have didReceiveData merely append to that NSMutableData;
Only when connectionDidFinishLoading: (in NSURLConnection) or URLSession:task:didCompleteWithError: (in NSURLSession) is called, should you then use the NSMutableData.
Alternatively, if using the block-based NSURLSession, this concern is completely eliminated (since you're not implementing any delegate methods). Using completionHandler-based methods of NSURLSession is much easier.
If all of this is too complicated, you might consider using AFNetworking's AFHTTPSessionManager (but not AFHTTPRequestOperationManager) to build your requests. It gets you out of the weeds of properly building requests, implementing delegate methods, etc.
You might need to wrap the strings into a dictionary and get the NSData object from a call to NSJSONSerialization. Though it depends on the form expected by the server.

set flag before connection start

in viewDidLoad I want to call webservice for several times.
So my code in viewDidLoad is as follows
//Webservice call for industry list
NSURL *aUrl = [NSURL URLWithString:[NSString stringWithFormat:#"%#index.php/industry/industrylist",baseurl]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:aUrl
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
[request setHTTPMethod:#"POST"];
[request setHTTPBody:[#"emailid=b#b.com&password=1234" dataUsingEncoding:NSUTF8StringEncoding]];
NSURLConnection *connection= [[NSURLConnection alloc] initWithRequest:request delegate:self];
//set flag for industryList
flag = #"industry";
[connection start];
//Webservice call for function list
NSURL *bUrl = [NSURL URLWithString:[NSString stringWithFormat:#"%#index.php/functionmdl/allFunctionlist",baseurl]];
NSMutableURLRequest *requestb = [NSMutableURLRequest requestWithURL:bUrl
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
[requestb setHTTPMethod:#"POST"];
[requestb setHTTPBody:[#"" dataUsingEncoding:NSUTF8StringEncoding]];
NSURLConnection *connectionb = [[NSURLConnection alloc] initWithRequest:requestb delegate:self];
//set flag for industryList
flag = #"functionmdl";
[connectionb start];
But the value of flag is always set to functionmdl because i have assigned the flag with the string at the just previous line of the last line in the above code. I know that i am setting the flag in wrong way. So, please let me know how can i set flag here. Basically i want to use these flag in connectionDidFinishLoading method.
i have to differentiate the webservice response data according to webservice call.
Please help me to resolve this.
I think you can use the connection.currentRequest.URL to distinguish the request.
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSString *strUrl = [connection.currentRequest.URL absoluteString] ;
// compare to the url of your request to distinguish them
}
There are several approaches here (none involve a flag).
The easiest is typically to use [NSURLConnection sendAsynchronousRequest:queue:completionHandler]. Then you can put your connectionDidFinishLoading code right here, specific to each request.
That doesn't work if you need more advanced features of NSURLConnectionDelegate (like responding to authentication requests). In that case, I usually recommend that you wrap up the connection and delegate into a separate object, and instantiate one for each connection. That way each object is only delegate for a single connection.
In a small number of cases, this still isn't appropriate, and in those cases you can check the connection's request in the delegate (connection.currentRequest) to determine which one you're being called about. In some cases, I've created a mutable dictionary property mapping NSURLRequest information to some other piece of metadata I wanted in the handlers.
And in the most fancy (and therefore least-often used) case, you can attach metadata (such as an identifier) to your connection using objc_setAssociatedObject, but this is seldom necessary.
But I'd look at [NSURLConnection sendAsynchronousRequest:queue:completionHandler]. It's the simplest to use and addresses the most common cases easily.

HTTP Status Code 411 - Length Required

I try to get data from server. I use NSURLConnectionDelegate, NSURLConnectionDataDelegate. There is code (Objective - C).
-(void)sendRequest
{
NSURL* url = [[NSURL alloc] initWithString:#"http://SomeServer"];
NSMutableURLRequest* request = [[NSMutableURLRequest alloc] init];
NSString* reqBody = [NSString stringWithFormat:#"<tag>Content</tag>"];
NSData* reqData = [reqBody dataUsingEncoding:NSUTF8StringEncoding];
NSInputStream* stream = [NSInputStream inputStreamWithData:reqData];
[request setURL:url];
[request setHTTPBodyStream:stream];
[request setHTTPMethod:#"POST"];
self.wpData = [[NSMutableData alloc] init];
NSURLConnection* conection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
[conection start];
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
[self.wpData setLength:0];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)d {
NSString* str = [[NSString alloc] initWithData:d encoding:NSUTF8StringEncoding];
NSLog(#"RESPONSE DATA: %#",str);
[self.wpData appendData:d];
}
But I get "411 - Length Required" when I use
[request setHTTPBodyStream:stream];
and "HASH (someAddress)" when I use
[request setHTTPBody:reqData];
I tried
[request setHTTPBodyStream:stream];
NSString *postLength = [NSString stringWithFormat:#"%d", [reqData length]];
[request setValue:postLength forHTTPHeaderField:#"Content-Length"];
but again "HASH (someAdddress)"
What have I done wrong?
Sorry for my English. Thanks :)
What have I done wrong?
Nothing.
HTTP Status Code 411 (Length Required) is sent by the server as a response when it refuses to accept a message without a content-length header, for whatever reason.
A server simply may or may not accept a content without a Content-Length header.
When you set an NSInputStream object as request body via property HTTPBodyStream for the request, NSURLConnection cannot evaluate the length of the body itself anymore. (there is no property length for a stream). Hence, NSURLConnection uses a certain "transfer mode", namely "chunked transfer encoding". This transfer mode should succeed to transmit any body and it does not require a Content-Legth header (actually must not contain one). Alas, the server simply does not accept this type of transfer.
See also: Chunked transfer encoding (wiki).
To solve the issue on the client side:
Determine the length of the body yourself (if possible) and set a "Content-Length" header field for the request. If this input stream has been created from a file or from a NSData object, the length can be easily determined. But be sure to set the exact same length as the actual stream content in bytes.
Don't use a NSInputStream, but use a NSData object as body and set it via property HTTPBody. When you set the body as a NSData object, NSURLConnection can determine the content length itself, and it will automatically add a Content-Length header with the correct length, unless you set it yourself in the request.

Using NSURLConnection with iOS 5 doesn't work properly

UPDATE: Apparently on iOS 5 the problem is the "Chunked-Encoding", When sending without that everything works. Seems on server that for some reason on iOS 5 the transfer never ends (on iOS 6 everything works). Anyone has a way around that?
I'm using NSURLConnection which works perfectly on iOS 6 and on simulator on same version, But when testing that on earlier devices I get response with only
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
and never with
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
Which suppose to contain my relevant data.
Here is a snippet of my code with all functions I've used (I saw that for some people removing some delegate function solved similar issue but in my case I don't have them):
-(void)establishConnection{
NSURL *url;
url = .... // Here I've set my url - it's https
self.responseData = [[NSMutableData alloc] initWithLength:0] ;
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:SERVER_RESPONSE_TIMEOUT];
[request setHTTPMethod:#"POST"];
// More settings here //
....
//Accept-Language: ENUS
[request addValue:#"ENUS" forHTTPHeaderField:#"Accept-Language"];
// "Accept-Topic: Dictation"
[request addValue:#"Dictation" forHTTPHeaderField:#"Accept-Topic"];
// "Accept: text/plain"
[request addValue:#"text/plain" forHTTPHeaderField:#"Accept"];
//"Transfer-Encoding: chunked"
[request addValue:#"chunked" forHTTPHeaderField:#"Transfer-Encoding"];
NSMutableData *postBody = [NSMutableData data];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [NSString stringWithFormat:#"%#",[paths objectAtIndex:0]]; // Get sound directory
NSData *soundData = [NSData dataWithContentsOfFile: [NSString stringWithFormat:#"%#/%#",documentsDirectory, #"rec.wav"]];
[postBody appendData:soundData];
[postBody appendData:[#"\r\n" dataUsingEncoding: NSUTF8StringEncoding]];
// final boundary
//[postBody appendData:[[NSString stringWithFormat:#"--%#\r\n", stringBoundary] dataUsingEncoding:NSUTF8StringEncoding]];
// add body to post
[request setHTTPBody:postBody];
self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES];
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
// You may have received an HTTP 200 here, or not...
NSLog(#"didReceiveResponse");
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
NSString* aStr = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
NSLog(#"This is my first chunk %#", aStr);
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connectionV {
connectionV = nil;
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(#"Something went wrong...");
}
Please help I can't find what am I doing wrong.
You should not set Transfer-Encoding chunked yourself. NSURLConnection will set this for you when it is appropriate.
Basically, a HTTP message requires either a Content-Length header set, or it uses chunked transfer encoding where no Content-Length header must be set.
When you set the body data as a stream via request's property HTTPBodyStream AND do NOT specify the Content-Length explicitly, NSURLConnection will automatically use chunked transfer encoding and basing its decision when the body data is finished on the stream's state (detecting EOF).
Otherwise, if you set the body data via property HTTPBody with a NSData object, you might set the Content-Length explicitly, or let NSURLConnection set it for you, based on the length of the NSData object. In that case, you don't get a chunked transfer encoding.
Otherwise, if you set your body data as a stream (say a NSInputStream which you created as a file stream) AND set the Content-Length header explicitly, NSURLConnection will NOT use chunked transfer encoding.
If possible, do set the Content-Length even for an NSInputStream, that is when you are able to know in advance how large the body is. There might be servers which have trouble or are simply not capable to parse data transmitted via chunked transfer encoding, e.g. Rails with a "simple server" like WEBrick when you send JSON or XML data. Otherwise, the web server will buffer all input data anyway.

Resources