Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AFNetworking 2.0 multipart request body blank

Similar to this issue.

Using AFNetworking 2.0.3 and trying to upload an image using AFHTTPSessionManager's POST + constructingBodyWithBlock. For reasons unknown, it seems as though the HTTP post body is always blank when the request is made to the server.

I subclass AFHTTPSessionManager below (hence the usage of [self POST ...].

I've tried constructing the request two ways.

Method 1: I just tried to pass params and then add only the image data should it exist.

- (void) createNewAccount:(NSString *)nickname accountType:(NSInteger)accountType primaryPhoto:(UIImage *)primaryPhoto
{
    NSString *accessToken = self.accessToken;

    // Ensure none of the params are nil, otherwise it'll mess up our dictionary
    if (!nickname) nickname = @"";
    if (!accessToken) accessToken = @"";

    NSDictionary *params = @{@"nickname": nickname,
                             @"type": [[NSNumber alloc] initWithInteger:accountType],
                             @"access_token": accessToken};
    NSLog(@"Creating new account %@", params);

    [self POST:@"accounts" parameters:params constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
        if (primaryPhoto) {
            [formData appendPartWithFileData:UIImageJPEGRepresentation(primaryPhoto, 1.0)
                                        name:@"primary_photo"
                                    fileName:@"image.jpg"
                                    mimeType:@"image/jpeg"];
        }
    } success:^(NSURLSessionDataTask *task, id responseObject) {
        NSLog(@"Created new account successfully");
    } failure:^(NSURLSessionDataTask *task, NSError *error) {
        NSLog(@"Error: couldn't create new account: %@", error);
    }];
}

Method 2: tried to build the form data in the block itself:

- (void) createNewAccount:(NSString *)nickname accountType:(NSInteger)accountType primaryPhoto:(UIImage *)primaryPhoto
{
    // Ensure none of the params are nil, otherwise it'll mess up our dictionary
    if (!nickname) nickname = @"";
    NSLog(@"Creating new account %@", params);

    [self POST:@"accounts" parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
        [formData appendPartWithFormData:[nickname dataUsingEncoding:NSUTF8StringEncoding] name:@"nickname"];
        [formData appendPartWithFormData:[NSData dataWithBytes:&accountType length:sizeof(accountType)] name:@"type"];
        if (self.accessToken)
            [formData appendPartWithFormData:[self.accessToken dataUsingEncoding:NSUTF8StringEncoding] name:@"access_token"];
        if (primaryPhoto) {
            [formData appendPartWithFileData:UIImageJPEGRepresentation(primaryPhoto, 1.0)
                                        name:@"primary_photo"
                                    fileName:@"image.jpg"
                                    mimeType:@"image/jpeg"];
        }
    } success:^(NSURLSessionDataTask *task, id responseObject) {
        NSLog(@"Created new account successfully");
    } failure:^(NSURLSessionDataTask *task, NSError *error) {
        NSLog(@"Error: couldn't create new account: %@", error);
    }];
}

Using either method, when the HTTP request hits the server, there is no POST data or query string params, only HTTP headers.

Transfer-Encoding: Chunked
Content-Length: 
User-Agent: MyApp/1.0 (iPhone Simulator; iOS 7.0.3; Scale/2.00)
Connection: keep-alive
Host: 127.0.0.1:5000
Accept: */*
Accept-Language: en;q=1, fr;q=0.9, de;q=0.8, zh-Hans;q=0.7, zh-Hant;q=0.6, ja;q=0.5
Content-Type: multipart/form-data; boundary=Boundary+0xAbCdEfGbOuNdArY
Accept-Encoding: gzip, deflate

Any thoughts? Also posted a bug in AFNetworking's github repo.

like image 336
Mike Sukmanowsky Avatar asked Jan 06 '14 18:01

Mike Sukmanowsky


2 Answers

Rob is absolutely right, the problem you're seeing is related to the (now closed) issue 1398. However, I wanted to provide a quick tl;dr in case anyone else was looking.

First, here's a code snippet provided by gberginc on github that you can model your file uploads after:

NSString* apiUrl = @"http://example.com/upload";

// Prepare a temporary file to store the multipart request prior to sending it to the server due to an alleged
// bug in NSURLSessionTask.
NSString* tmpFilename = [NSString stringWithFormat:@"%f", [NSDate timeIntervalSinceReferenceDate]];
NSURL* tmpFileUrl = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:tmpFilename]];

// Create a multipart form request.
NSMutableURLRequest *multipartRequest = [[AFHTTPRequestSerializer serializer] multipartFormRequestWithMethod:@"POST"
                                                                                                   URLString:apiUrl
                                                                                                  parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> formData)
                                         {
                                             [formData appendPartWithFileURL:[NSURL fileURLWithPath:filePath]
                                                                        name:@"file"
                                                                    fileName:fileName
                                                                    mimeType:@"image/jpeg" error:nil];
                                         } error:nil];

// Dump multipart request into the temporary file.
[[AFHTTPRequestSerializer serializer] requestWithMultipartFormRequest:multipartRequest
                                          writingStreamContentsToFile:tmpFileUrl
                                                    completionHandler:^(NSError *error) {
                                                        // Once the multipart form is serialized into a temporary file, we can initialize
                                                        // the actual HTTP request using session manager.

                                                        // Create default session manager.
                                                        AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];

                                                        // Show progress.
                                                        NSProgress *progress = nil;
                                                        // Here note that we are submitting the initial multipart request. We are, however,
                                                        // forcing the body stream to be read from the temporary file.
                                                        NSURLSessionUploadTask *uploadTask = [manager uploadTaskWithRequest:multipartRequest
                                                                                                                   fromFile:tmpFileUrl
                                                                                                                   progress:&progress
                                                                                                          completionHandler:^(NSURLResponse *response, id responseObject, NSError *error)
                                                                                              {
                                                                                                  // Cleanup: remove temporary file.
                                                                                                  [[NSFileManager defaultManager] removeItemAtURL:tmpFileUrl error:nil];

                                                                                                  // Do something with the result.
                                                                                                  if (error) {
                                                                                                      NSLog(@"Error: %@", error);
                                                                                                  } else {
                                                                                                      NSLog(@"Success: %@", responseObject);
                                                                                                  }
                                                                                              }];

                                                        // Add the observer monitoring the upload progress.
                                                        [progress addObserver:self
                                                                   forKeyPath:@"fractionCompleted"
                                                                      options:NSKeyValueObservingOptionNew
                                                                      context:NULL];

                                                        // Start the file upload.
                                                        [uploadTask resume];
                                                    }];

And secondly, to summarize the problem (and why you have to use a temporary file as a work around), it really is two fold.

  1. Apple considers the content-length header to be under its control, and when a HTTP body stream is set for a NSURLRequest Apple's libraries will set the encoding to Chunked and then abandon that header (and thereby clearing any content-length value AFNetworking sets)
  2. The server the upload is hitting doesn't support Transfer-Encoding: Chunked (eg. S3)

But it turns out, if you're uploading a request from a file (because the total request size is known ahead of time), Apple's libraries will properly set the content-length header. Crazy right?

like image 187
Mr. T Avatar answered Oct 22 '22 00:10

Mr. T


Digging into this further, it appears that when you use NSURLSession in conjunction with setHTTPBodyStream, even if the request sets Content-Length (which AFURLRequestSerialization does in requestByFinalizingMultipartFormData), that header is not getting sent. You can confirm this by comparing the allHTTPHeaderFields of the task's originalRequest and currentRequest. I also confirmed this with Charles.

What's interesting is that Transfer-Encoding is getting set as chunked (which is correct in general when the length is unknown).

Bottom line, this seems to be a manifestation of AFNetworking's choice to use setHTTPBodyStream rather than setHTTPBody (which doesn't suffer from this behavior), which, when combined with NSURLSession results in this behavior of malformed requests.

I think this is related to AFNetworking issue 1398.

like image 28
Rob Avatar answered Oct 22 '22 00:10

Rob