Coming from the world of web programming, I'm pretty much comfortable with working with multipart form requests to upload files. However, in iOS, we have a thing called NSURLSession with the method uploadTaskWithRequest, which seems to be the method to call to do image uploads and the likes.
Can you explain the difference between the two approach, multipart form upload vs uploadTaskWithRequest? If I already have a backend that handle multipart form uploads, what kind of adjustments that I might need so that it support uploadTaskWithRequest as well?
The uploadTaskWithRequest simply sends the NSData, file, or stream as the body of the request. It doesn't do anything beyond that. It simply has the benefit that it can be used with background sessions.
So, if you have web service that is expecting multipart/form-data request, you have to build that request yourself (unless you use something like AFNetworking or Alamofire to do this for you). Once you've built that request, you can either use dataTaskWithRequest (having set the HTTPBody of the NSMutableURLRequest) or uploadTaskWithRequest (in which case you don't set HTTPBody, but rather provide it as a parameter to uploadTaskWithRequest).
By the way, a tool like Charles is very useful in these cases, letting you observe what's going on behind the scenes.
File Upload with multipart/form-data
The first approach using a multipart/form-data Content-type was originally defined in RFC 1867, then moved to the World Wide Web Consortium, which included it in the specification for HTML 4.0, where forms are expressed in HTML and where form values are sent via HTTP and electronic mail. When the form has been filled out by a user the form was sent to the server. This technique is widely supported and used by browsers and web servers.
However multipart/form-data can also be used to define form data which are presented in other representations than HTML. That is, you don't necessarily need a web browser or web server. The current specification which can be used by a wide variety of applications and transported by a wide variety of protocols is RFC 7578 (form IETF).
It must be mentioned, though, that the multipart/form-data content type was not always/is not without issues. It is quite complex by itself. Additionally, it uses/refers to a lot of other RFCs and - as a result of clearing things up - it and those which it depends on have been changed, obsoleted and updated quite frequently. Due to its complexity, serialisers and parsers are getting quite complicated, too and there's a lot of room for bugs and other issues.
NSURLSession uploadTaskWithRequest
How NSURLSession composes a request is not precisely documented. It certainly does not use a multipart/form-data content type, though.
For upload tasks, NSURLSession uses a POST request with a NSURLRequest as parameter which you can setup yourself. That is, you optionally can set the content type (for example text/plain; charset=utf-8), and other headers. NSURLSession can also derive an appropriate content type itself from the given content (file, stream or NSData). That is, we may say, it becomes a "simple" POST request. Due to less complexity, the request is less troublesome.
So, in order for your server to support an uploadTaskWithRequest where a file should be uploaded, it should simply support a POST request with some "simple" content type. That is, as opposed to a "file upload" with a multipart/form-data content type which contains the file name in a disposition header, the server would need to return the URL of the location where the resource (the file) has been written to.
Fortunately, it is quite easy to do a multipart/form-data POST request in the background, for example if you want to upload an image along with some other information.
First, create the NSMutableURLRequest in the same way like you would for a synchronous request (see for example POST multipart/form-data with Objective-C).
Then, write the body of the request to a file and supply it to the uploadTaskWithRequest method of the NSURLSession that you created with a backgroundSessionConfiguration:
NSString *filePath = [[NSSearchPathForDirectoriesInDomains(
NSCachesDirectory, NSUserDomainMask, YES) lastObject]
stringByAppendingPathComponent:imageUUID];
[request.HTTPBody writeToFile:filePath atomically:YES];
NSURLSessionUploadTask *task = [urlSession uploadTaskWithRequest:request
fromFile:[NSURL fileURLWithPath:filePath]];
[task resume];
If you have multiple tasks and want to be able to distinguish them in the delegate callback, you can set a parameter (after you have created the request) with the NSURLProtocol class:
[NSURLProtocol setProperty:imageUUID
forKey:#"yourKeyForTheImageUUID"
inRequest:request];
and get it back in the callback like this:
- (void)URLSession:(NSURLSession *)session
task:(nonnull NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error
{
NSString *imageUUID = [NSURLProtocol propertyForKey:#"yourKeyForTheImageUUID"
inRequest:task.originalRequest];
Related
I'm trying to understand how to properly use NSURLSession for my scenario, reading through specification, need more clarification..
https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/URLLoadingSystem/Articles/UsingNSURLSession.html#//apple_ref/doc/uid/TP40013509-SW1
My server API is very simple. I use protobufs for data communication and message payload very small, turnaround very quick. From data standpoint it's just plain binary data being transferred.
Server supports only POST request and responds with data.
So, it goes like this:
- POST request with custom headers and binary payload
- server responds with message and binary payload (response also might include custom headers)
From what I see in documentation data tasks is exactly what I need, but they say
Data tasks send and receive data using NSData objects. Data tasks are
intended for short, often interactive requests from your app to a
server. Data tasks can return data to your app one piece at a time
after each piece of data is received, or all at once through a
completion handler. Because data tasks do not store the data to a
file, they are not supported in background sessions
So, I left with download and upload tasks and they go into a file. How do I go about achieving what I need? Sounds like I should use upload task, but will I get response data back?
ok. I decided not to delete my question in case someone else needs this info.
Same documentation article says:
Uploading a File Using a Download Task To upload body content for a
download task, your app must provide either an NSData object or a body
stream as part of the NSURLRequest object provided when it creates the
download request.
If you provide the data using a stream, your app must provide a
URLSession:task:needNewBodyStream: delegate method to provide a new
body stream in the event of an authentication failure. This method is
described further in “Uploading Body Content Using a Stream.”
The download task behaves just like a data task except for the way in
which the data is returned to your app.
I need to upload a file to a server and the request has to specify some form parameters. How can I do this with the NSURLSessionUploadTask? In other words, how can I send a multipart POST request.
cheers
When using NSURLSessionUploadTask, you don't call setHTTPBody on the NSMutableURLRequest (like we used to do with NSURLConnection), but rather you include this in the NSData or file passed to the NSURLSession method, uploadTaskWithRequest (or in the stream we supply for uploadTaskWithStreamedRequest).
In terms of creating the body of the request, itself, you have to do this yourself. Obviously, a framework like AFNetworking can greatly simplify this process.
https://github.com/pyke369/PKMultipartInputStream
This library can generate the multipart request pretty well.
I'm looking for a way to use NSMutableURLRequest with app level proxyHost/Port settings, essentially a replacement for ASIHTTPRequest lib with proxyHost/proxyPort. I've tried modifying the CFReadStream (from NSURLRequest HTTPBodyStream), but it SIGSEGs when setting the proxy settings. I would rather not have to rewrite my app with CFNetworking, and it looks like AFNetwork lib doesn't include this feature yet either.
Has anyone successfully done this with NSMutableURLRequest?
The real answer appears to be creating a custom NSURLProtocol. Should be a straight forward derivation, and add the appropriate proxyHost/proxyPort to the request (along with any other values such as a customized User Agent string). Then, supposedly, all requests will be routed through this custom protocol (including UIWebView requests..direct or derived).
relevant samples:
https://developer.apple.com/library/ios/samplecode/CustomHTTPProtocol/Introduction/Intro.html#//apple_ref/doc/uid/DTS40013653
http://eng.42go.com/customizing-uiwebview-requests-with-nsurlprotocol/
I'll post more when I have the thing operational.
Things of note with this implementation.
Initially started using CFNetworking as the "wedge" in my custom
NSURLPRotocol, but quickly found I was rewriting the same code that
was in ASIHTTPRequest. So I just implemented the wedge with
ASIHTTPRequest.
The items that are not documented well (or at all), is the
interaction of UIWebView with NSURLProtocol callbacks, vs
NSURLRequest/Conenction with NSURLProtocol. Some Findings:
a) All dependent page resources are loaded automatically by UIWebView
(which we knew), and they all go through NSURLProtocol, so it is an
excellent place to put in code to modify all requests.
b) The UIWebView sets the Referer header. On a redirect, the only
way to get the UIWebView to update it's Referer from the original URL
to the new redirect URL is with the [[self client] URLProtocol:self
wasRedirectedToRequest:redirectRequest
redirectResponse:tmpHttpResponse]; callback.
c) when the above redirect callback is received by UIWebView, it
generates a new NSURLRequest (essentially the one you sent back to
it). So if you have a wedge that likes to do the redirect
internally, you have to cancel the Request that it attempts to make,
in favor of the new one from UIWebView.
You have to be careful with which callbacks you implement from ASIHTTPRequestDelegate. e.g. implementing didReceiveData will disable the built in gzip processing. willRedirectToURL disables most of the built in redirect processing (even if you call [request redirectToURL:newURL]; as recommended in the comments).
I have to call a third party webservice that expects and only accepts illegal characters (according to the RFC) in it's param string, like the below.
http://example.com?param1={foo=bar}
In this example the braces are the illegal characters, and should be encoded, however this webservice will not accept the parameters if these characters have been encoded.
NSURL correctly doesn't allow me to create a NSURL object with the URLWithString method, return nil using a string like the example.
The webservice is provided by a large corporate entity, so changing it would require submitting a bug report to them, which may or may not be actioned soon, if at all, especially considering that the API works as is.
My question is what are some possible solutions to this problem, that i can implement, without changing the Webservice.
Current ideas (downsides)
Using CFStream to craft custom HTTP requests (Horrifically large amount of work)
Using a webbased proxy that could send the request on my application's behalf (Additional external dependency)
Thanks
What you are trying to do is impossible. You need to URL-encode these characters and the server will then automatically decode them.
If you could somehow hack NSURL there would be still many parts of the whole process that would choke on a malformed URL.
As a novice iOS developer, I am trying to understand some concepts related to the callback mechanisms in iOS.
My model makes HTTP requests through NSURLRequest to a backend rest service. The model has several methods which corresponds to the methods in the service. NSURLRequest is based on the delegate pattern, which means that I receive a common callback for all of the service calls. Then, my model has to find out which service call the callback is related to, so that I can send an appropriate update event to the controller. This is awkward since I have to maintain som state in the model to remember which call I made the last time (which is very impractical in the case of concurrency), or interpret the payload in the HTTP response.
I would wish that NSURLRequest would support the target-action pattern, so that each of the requests could decide which callback method to use. Is that possible? Am I missing something here?
If target-action is not available in the framework, what are the best practices to solve this?
The way to do this is to use NSURLRequest with NSURLConnection. If you check out the docs for NSURLConnection, they will tell you that you need to implement the callback methods in the NSURLConnectionDelegate protocol, and will give you details.
That page also points to several examples, with sample code.
You can also check out the URL Loading System Programming Guide at developer.apple.com, which will give you additional information on how these classes are intended to be used.