AFNetworking 2.0 多部分请求正文空白

Posted

技术标签:

【中文标题】AFNetworking 2.0 多部分请求正文空白【英文标题】:AFNetworking 2.0 multipart request body blank 【发布时间】:2014-01-06 18:09:12 【问题描述】:

类似于this issue。

使用 AFNetworking 2.0.3 并尝试使用 AFHTTPSessionManager 的 POST +constructingBodyWithBlock 上传图像。由于未知原因,当向服务器发出请求时,HTTP 帖子正文似乎总是空白。

我在下面子类化 AFHTTPSessionManager(因此使用了[self POST ...]

我尝试了两种方式构建请求。

方法1:我只是尝试传递参数,然后只添加存在的图像数据。

- (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);
    ];

方法2:尝试在block本身中构建表单数据:

- (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);
    ];

使用任一方法,当 HTTP 请求到达服务器时,没有 POST 数据或查询字符串参数,只有 HTTP 标头。

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

有什么想法吗?还在AFNetworking's github repo 中发布了一个错误。

【问题讨论】:

我使用AFHTTPRequestOperationManager 创建了多部分请求,它运行良好,但是当我使用几乎相同的AFHTTPSessionManager 时,它失败了。 AFHTTPSessionManager 内部似乎有问题。 这成功了@Rob,现在将问题悬而未决,但这绝对闻起来像一个错误。 你试过查看task.response吗?我的返回 statusCode 为 406(但来自 AFHTTPRequestOperationManager 的相同请求很好)。 我的返回 401 Unauthorized,因为 access_token 没有随请求一起发送。 406 error 听起来很奇怪。显然,您的服务器担心它对未处理客户端的响应。您是否在请求上设置了任何特殊的 HTTP 标头? 没有。我的客户端代码与上面的第一个示例完全相同。 (我正在测试您的代码。)我添加的唯一内容是明确指定请求和响应序列化程序分别为AFHTTPRequestSerializerAFHTTPResponseSerializer。同样的请求(以及序列化程序的使用)与AFHTTPRequestOperationManager 一起工作得很好。我正在调试两个 AFNetworking 管理器类,看看是否能看到两者之间差异的根源。 【参考方案1】:

Rob 是绝对正确的,您看到的问题与(现已关闭)issue 1398 有关。但是,我想提供一个快速的 tl;dr 以防其他人正在寻找。

首先,这是gberginc on github 提供的代码 sn-p,您可以在此之后对文件上传进行建模:

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];
                                                    ];

其次,总结一下这个问题(以及为什么必须使用临时文件来解决问题),它确实是双重的。

    Apple 认为 content-length 标头在其控制之下,并且当为 NSURLRequest 设置 HTTP 主体流时,Apple 的库会将编码设置为 Chunked,然后放弃该标头(从而清除任何 content-length价值 AFNetworking 集) 上传命中的服务器不支持Transfer-Encoding: Chunked(例如S3)

但事实证明,如果您从文件上传请求(因为提前知道总请求大小),Apple 的库将正确设置 content-length 标头。疯了吧?

【讨论】:

【参考方案2】:

进一步深入研究,似乎当您将NSURLSessionsetHTTPBodyStream 结合使用时,即使请求设置Content-LengthAFURLRequestSerializationrequestByFinalizingMultipartFormData 中所做的),该标头也不会被发送.您可以通过比较任务的originalRequestcurrentRequestallHTTPHeaderFields 来确认这一点。我也向查尔斯证实了这一点。

有趣的是Transfer-Encoding 被设置为chunked(通常在长度未知时是正确的)。

底线,这似乎是 AFNetworking 选择使用 setHTTPBodyStream 而不是 setHTTPBody(不会受到这种行为的影响)的一种表现,当与 NSURLSession 结合使用时,会导致这种格式错误的行为请求。

我认为这与 AFNetworking issue 1398 有关。

【讨论】:

【参考方案3】:

我自己也遇到了这个问题,正在尝试这两种方法和这里建议的方法......

事实证明,只需将附加数据的“名称”键更改为“文件”而不是文件名变量即可。

确保您的数据键匹配,否则您将看到空数据集的相同症状。

【讨论】:

以上是关于AFNetworking 2.0 多部分请求正文空白的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 AFNetworking-2.0 执行 JSON 编码的 GET 请求?

AFNetworking 2.0:多部分 POST 时出现错误 503

AFNetworking 2.0 多部分/表单数据上传到 mySQL

AFNetworking 2.0 中的 AFHTTPRequestOperationManager 如何传递 HTTPBody 标签

Swift 2.0 POST 正文空

使用 AFNetworking 的多部分 PUT 请求