7

edit: I need to upload a file asynchronously from an iPhone to a Python server-side process. I'd like to do the request asynchronously so that I can display a busy animation while it's working.

The request needs to include the username, password and file as 'multipart/form-data'.

I can get it working synchronously using NSURLConnection with the code looking like this::

-(void) uploadDatabase{

Database *databasePath = [[Database alloc] init];
NSString *targetPath = [databasePath getPathToDatabaseInDirectory];

NSData *dbData = [NSData dataWithContentsOfFile:targetPath];
NSString *url = @"http://mydomain.com/api/upload/";
//NSString *username = [[NSUserDefaults standardUserDefaults] stringForKey:USERNAME];
NSString *username = @"user";
NSString *password = @"pass";
NSMutableURLRequest *request = [self createRequestForUrl:url withUsername:username andPassword:password andData:dbData];

NSURLResponse *response;
NSError *error;

NSData *result = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];

NSString *stringResult = [[NSString alloc] initWithData:result encoding:NSUTF8StringEncoding];

NSLog(@"**server info %@", stringResult);}

// Request construction

    -(NSMutableURLRequest*) createRequestForUrl: (NSString*)urlString withUsername:(NSString*)username andPassword:(NSString*)password andData:(NSData*)dbData
    {NSURL *url = [NSURL URLWithString:urlString];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:60.0];
[request setHTTPMethod:@"POST"];

NSString *boundary = @"BOUNDARY_STRING";
NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary];
[request addValue:contentType forHTTPHeaderField:@"Content-Type"];

NSMutableData *body = [NSMutableData data];

if(dbData != NULL)
{
    //only send these methods when transferring data as well as username and password
    [body appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
    [body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"file\"; filename=\"dbfile\"\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];
    [body appendData:[@"Content-Type: application/octet-stream\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
    [body appendData:[NSData dataWithData:dbData]];
}

[body appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
[body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"username\"\r\n\r\n%@", username] dataUsingEncoding:NSUTF8StringEncoding]];

[body appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
[body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"password\"\r\n\r\n%@", password] dataUsingEncoding:NSUTF8StringEncoding]];

[body appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];

[request setHTTPBody:body];

return request;}

However, when I try to do this asynchronously using NSURLSession it doesn't seem to work properly. The code with NSURLSession looks like this:

    -(void)uploadDatabase{
    Database *databasePath = [[Database alloc] init];
    NSString *targetPath = [databasePath getPathToDatabaseInDirectory];
    NSURL *phonedbURL = [NSURL URLWithString:targetPath];

    NSString *url = @"http://mydomain.com/api/upload/";
    NSString *username = @"user";
    NSString *password = @"pass";
    NSMutableURLRequest *request = [self createRequestForUrl:url withUsername:username andPassword:password andData:NULL];

NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];

self.uploadSession = [NSURLSession sessionWithConfiguration:defaultConfigObject delegate:self delegateQueue:Nil];
NSLog(@"the url = %@",url);
NSURLSessionUploadTask *uploadTask = [self.uploadSession uploadTaskWithRequest:request fromFile:phonedbURL];

[uploadTask resume];}

I'm struggling to see what I'm doing differently though as it seems this should work.

Is using NSURLSession the right way to do asynchronous requests? and I'm new to NSURLSession so do I have to change my NSURLMutableRequest for NSURLSession requests rather than NSURLConnection?

Thanks in advance for any help!

Shaik Riyaz
  • 11,204
  • 7
  • 53
  • 70
user2224540
  • 73
  • 1
  • 4
  • Is there a particular reason you're using an NSURLSessionConfiguration, rather than just replacing sendSynchronousRequest: with sendAsynchronousRequest:? – rdelmar Jan 02 '14 at 23:42
  • I tried that as well - it didn't seem to work either so I used NSURLSession instead as it seemed it is the most up to date methods to be using - is that a mistake? – user2224540 Jan 03 '14 at 01:00
  • I don't know if it's a mistake, I've never used an NSURLSessionConfiguration. It seems that changing from a synchronous to asynchronous send, is a simpler substitution. What do you mean by it didn't work? What result did you get? – rdelmar Jan 03 '14 at 01:03

1 Answers1

25

You are correct, that if you just want to make your request asynchronous, you should retire sendSynchronousRequest. While we once would have recommended sendAsynchronousRequest, effective iOS 9, NSURLConnection is formally deprecated and one should favor NSURLSession.

Once you start using NSURLSession, you might find yourself drawn to it. For example, one can use a [NSURLSessionConfiguration backgroundSessionConfiguration:], then have uploads progress even after the app has gone into background. (You have to write a few delegate methods, so for simplicity's sake, I've stayed with a simple foreground upload below.) It's just a question of your business requirements, offsetting the new NSURLSession features versus the iOS 7+ limitation it entails.

By the way, any conversation about network requests in iOS/MacOS is probably incomplete without a reference to AFNetworking. It greatly simplifies creation of these multipart requests and definitely merits investigation. They have NSURLSession support, too (but I haven't used their session wrappers, so can't speak to it). But AFNetworking is undoubtedly worthy of your consideration. You can enjoy some of the richness of the delegate-base API (e.g. progress updates, cancelable requests, dependencies between operations, etc.), offering far greater control that available with convenience methods (like sendSynchronousRequest), but without dragging you through the weeds of the delegate methods themselves.

Regardless, if you're really interested in how to do uploads with NSURLSession, see below.


If you want to upload via NSURLSession, it is a slight shift in thinking, namely, separating the configuration of the request (and its headers) in the NSMutableURLRequest from the creation of the the body of the request (which you now specify during the instantiation of the NSURLSessionUploadTask). The body of the request that you now specify as part of the upload task can be either a NSData, a file, or a stream (I use a NSData below, because we're building a multipart request):

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:@"POST"];
NSString *boundary = [self boundaryString];
[request addValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary] forHTTPHeaderField:@"Content-Type"];

NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];

NSData *fileData = [NSData dataWithContentsOfFile:path];
NSData *data = [self createBodyWithBoundary:boundary username:@"rob" password:@"password" data:fileData filename:[path lastPathComponent]];

NSURLSessionUploadTask *task = [session uploadTaskWithRequest:request fromData:data completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
    NSAssert(!error, @"%s: uploadTaskWithRequest error: %@", __FUNCTION__, error);

    // parse and interpret the response `NSData` however is appropriate for your app
}];
[task resume];

And the creation of the NSData being sent is much like your existing code:

- (NSData *) createBodyWithBoundary:(NSString *)boundary username:(NSString*)username password:(NSString*)password data:(NSData*)data filename:(NSString *)filename
{
    NSMutableData *body = [NSMutableData data];

    if (data) {
        //only send these methods when transferring data as well as username and password
        [body appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
        [body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"file\"; filename=\"%@\"\r\n", filename] dataUsingEncoding:NSUTF8StringEncoding]];
        [body appendData:[[NSString stringWithFormat:@"Content-Type: %@\r\n\r\n", [self mimeTypeForPath:filename]] dataUsingEncoding:NSUTF8StringEncoding]];
        [body appendData:data];
        [body appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
    }

    [body appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
    [body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"username\"\r\n\r\n%@\r\n", username] dataUsingEncoding:NSUTF8StringEncoding]];

    [body appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
    [body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"password\"\r\n\r\n%@\r\n", password] dataUsingEncoding:NSUTF8StringEncoding]];

    [body appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];

    return body;
}

You hardcoded the boundary and the mime type, which is fine, but the above happens to use the following methods:

- (NSString *)boundaryString
{
    NSString *uuidStr = [[NSUUID UUID] UUIDString];

    // If you need to support iOS versions prior to 6, you can use
    // Core Foundation UUID functions to generate boundary string
    //
    // adapted from http://developer.apple.com/library/ios/#samplecode/SimpleURLConnections
    //
    // NSString  *uuidStr;
    //
    // CFUUIDRef uuid = CFUUIDCreate(NULL);
    // assert(uuid != NULL);
    // 
    // NSString  *uuidStr = CFBridgingRelease(CFUUIDCreateString(NULL, uuid));
    // assert(uuidStr != NULL);
    // 
    // CFRelease(uuid);

    return [NSString stringWithFormat:@"Boundary-%@", uuidStr];
}

- (NSString *)mimeTypeForPath:(NSString *)path
{
    // get a mime type for an extension using MobileCoreServices.framework

    CFStringRef extension = (__bridge CFStringRef)[path pathExtension];
    CFStringRef UTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, extension, NULL);
    assert(UTI != NULL);

    NSString *mimetype = CFBridgingRelease(UTTypeCopyPreferredTagWithClass(UTI, kUTTagClassMIMEType));
    assert(mimetype != NULL);

    CFRelease(UTI);

    return mimetype;
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • thanks! I chose to go with NSURLSession, your code works perfectly and highlighted why mine was not working before. I was not correctly importing the sqlite MIME type in my request. In case others have a similar problem uploading sqlite files, in addition to using @Rob's code I had to add a bit of code to my info.plist file, as per the answer to this post [link](http://stackoverflow.com/questions/3127355/associating-sqlite3-db-to-an-iphone-app) , so my app correctly recognised the sqlite MIME type. – user2224540 Jan 06 '14 at 11:51
  • 1
    Thank you for posting this! I had a different issue but it was `[request addValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary] forHTTPHeaderField:@"Content-Type"];` that did the trick for me. I had been setting `Content-Type` to `multipart/form-data` when creating the shared background session, via `configuration.HTTPAdditionalHeaders = @{...}` and noticed the server was still being told the request was of type `application/octet-stream`. Setting the `Content-Type` at the request level fixed this for me. – John Erck Jun 09 '14 at 11:26
  • @Rob Could you take a look at my code here and perhaps help explain why my upload works from html form to php but not from iOS using NSURLSession? Thx – marciokoko Aug 03 '14 at 18:59
  • 1
    @marciokoko With `NSURLSessionUploadTask`, you don't use `setHTTPBody` with the request. You put the body of the request in the `data` you supply to `uploadTaskWithRequest:fromData:`. If you use `uploadTaskWithRequest:fromFile:`, you have to take that `NSData` we previous used in `setHTTPBody`, and instead write it to a file, and then pass that file to `uploadTaskWithRequest:fromFile:`. It's unclear to me from your sample what that `phonedbURL` was. – Rob Aug 03 '14 at 20:37
  • My code is here: http://stackoverflow.com/questions/25098056/why-do-i-get-undefined-index-uploading-image-to-php-from-ios-but-not-from-html?noredirect=1#comment39054905_25098056. I don't have a phonedbURL. – marciokoko Aug 04 '14 at 01:41
  • 1
    The final code sample in your question above does have a variable called `phonedbURL`, which appears to be the location of some file. And you're passing that as the `fromFile` parameter of `uploadTaskWithRequest`. That's not correct. Furthermore, you're setting the `HTTPBody` of the `NSMutableURLRequest`, which you shouldn't do with a `NSURLSessionUploadTask`. Like I said, you create the body of the request, and in the case of `NSURLSessionUploadTask`, you specify that as the parameter to the `uploadTaskWithRequest` method (either as `fromData` or save it to a file and use `fromFile`). – Rob Aug 08 '14 at 04:03
  • Also, as an aside, when getting the URL of a file in your local file system, you should use `[NSURL fileURLWithPath:path]`, not `URLWithString`. – Rob Aug 08 '14 at 04:13
  • Again an awesome post from Rob. Thanks. – Matz Sep 13 '16 at 08:15