Sending Multipart Forms with Objective-C

Wednesday, September 12, 2012

Tags: apple, programming, objc

It took me a few evenings to figure this out so I’m writing a quick explanation based on what I’ve found to work. My use-case is pretty simple, I want to POST some data to a form on a server from an iOS app I’m building. I’ll be using NSURLRequest to build the request object and NSURLConnection to make the actual connection to the server.

The first thing we need to understand is how Multipart Form requests should be structured. The spec document explains these in detail so I’m just going to show at a high level how this is structured. To get started lets create an NSMutableURLRequest:

NSURL *url = [NSURL URLWithString:@"http://example.com/form/"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:@"POST"];

Now we need to define the content-type and a boundary string. I’m not really sure what a boundary string is, it just needs to be consistently represented throughout the request.

Content-Type: multipart/form-data; boundary=YOUR_BOUNDARY_STRING

To do this in objective-c we need to write the following:

NSURL *url = [NSURL URLWithString:@"http://example.com/form/"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:@"POST"];

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

Now let’s get look at the request body. Here’s a simple example of how it needs to look to the server:

--YOUR_BOUNDARY_STRING
Content-Disposition: form-data; name="photo"; filename="calm.jpg"
Content-Type: image/jpeg

YOUR_IMAGE_DATA_GOES_HERE
--YOUR_BOUNDARY_STRING
Content-Disposition: form-data; name="message"

My first message
--YOUR_BOUNDARY_STRING
Content-Disposition: form-data; name="user"

1
--YOUR_BOUNDARY_STRING

I’m sending over three variables: an image named photo, a string named message, and an integer named user. It’s important to note the linebreaks and the dashes before the boundary string. These must be included in order to build a good request. Now lets write some objective-c:

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

NSMutableData *body = [NSMutableData data];

[body appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
[body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"photo\"; filename=\"%@.jpg\"\r\n", self.message.photoKey] dataUsingEncoding:NSUTF8StringEncoding]];
[body appendData:[@"Content-Type: application/octet-stream\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
[body appendData:[NSData dataWithData:imageData]];

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

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

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

[request setHTTPBody:body];

Now all we need to do is make a connection to the server and send the request:

[request setHTTPBody:body];

NSURLResponse *response;
NSError *error;

[NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];

That’s it. A more detailed explanation of the request formatting can be found here.

If you’re curious, I’m posting to a Django app I was running locally so I could use ipdb and iPython to inspect the process and see what my app was posting. Then I compared that to a post request generated by a testcase I knew worked.