AFNetworking 2.0 multipart request body blank

2019-03-21 08:26发布

问题:

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.

回答1:

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?



回答2:

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.



回答3:

I was running into this problem myself, and was trying both methods and the suggested method here...

Turns out, it was as simple as changing the appended data "name" key to "file" instead of the filename variable.

Be sure your data key matches, or you will see the same symptom of an empty data set.