Пустое тело составного запроса AFNetworking 2.0

Аналогично этой проблеме.

Используя AFNetworking 2.0.3 и пытаясь загрузить изображение с помощью POST AFHTTPSessionManager + ConstructionBodyWithBlock. По неизвестным причинам кажется, что тело сообщения 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: пытался создать данные формы в самом блоке:

- (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 на github.


person Mike Sukmanowsky    schedule 06.01.2014    source источник
comment
Я создал составной запрос с AFHTTPRequestOperationManager, и он работал нормально, но когда я использовал почти идентичный AFHTTPSessionManager, он терпел неудачу. Похоже, внутри AFHTTPSessionManager что-то не так.   -  person Rob    schedule 06.01.2014
comment
Это помогло @Rob, пока я оставлю вопрос открытым, но это определенно пахнет ошибкой.   -  person Mike Sukmanowsky    schedule 07.01.2014
comment
Вы пробовали смотреть на task.response? Мой возвращает statusCode из 406 (но тот же запрос подходит для AFHTTPRequestOperationManager).   -  person Rob    schedule 07.01.2014
comment
Мой возвращал 401 Unauthorized, так как access_token не отправлялся вместе с запросом. ошибка 406 звучит странно. Очевидно, ваш сервер беспокоится о том, что его ответ на клиент не обрабатывается. Вы устанавливаете какие-либо специальные заголовки HTTP в запросе?   -  person Mike Sukmanowsky    schedule 07.01.2014
comment
Нет. Мой клиентский код точно такой же, как в первом примере выше. (Я тестировал ваш код.) Единственное, что я добавил, это явное указание сериализаторов запроса и ответа как AFHTTPRequestSerializer и AFHTTPResponseSerializer соответственно. И тот же запрос (и использование сериализаторов) отлично работал с AFHTTPRequestOperationManager. Я отлаживаю два класса менеджера AFNetworking, чтобы увидеть, в чем причина несоответствия между ними.   -  person Rob    schedule 07.01.2014
comment
Я думаю, что ответ 406 — это отвлекающий маневр. Основная проблема, ИМХО, заключается в том, что комбинация NSURLSession и использование AFNetworking setHTTPBodyStream (а не setHTTPBody) приводит к искаженным запросам. Смотрите мой ответ ниже.   -  person Rob    schedule 10.01.2014


Ответы (3)


Роб абсолютно прав, проблема, которую вы видите, связана с (теперь закрытой) проблемой 1398. Тем не менее, я хотел предоставить краткую справку на случай, если кто-то еще будет искать.

Во-первых, вот фрагмент кода, предоставленный gberginc на github, после которого вы можете смоделировать загрузку файлов:

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

И, во-вторых, если обобщить проблему (и почему вы должны использовать временный файл в качестве обходного пути), она действительно двояка.

  1. Apple считает, что заголовок длины содержимого находится под ее контролем, и когда основной поток HTTP установлен для NSURLRequest, библиотеки Apple установят кодировку в Chunked, а затем откажутся от этого заголовка (и тем самым очистив любое значение длины содержимого, установленное AFNetworking)
  2. Сервер, на который загружается загрузка, не поддерживает Transfer-Encoding: Chunked (например, S3).

Но оказывается, если вы загружаете запрос из файла (поскольку общий размер запроса известен заранее), библиотеки Apple правильно установят заголовок длины содержимого. Сумасшедший, верно?

person Mr. T    schedule 22.05.2014

Если углубиться в это, оказывается, что когда вы используете NSURLSession в сочетании с setHTTPBodyStream, даже если запрос устанавливает Content-Length (что AFURLRequestSerialization делает в requestByFinalizingMultipartFormData), этот заголовок не отправляется. Вы можете убедиться в этом, сравнив allHTTPHeaderFields originalRequest и currentRequest задачи. Я также подтвердил это с Чарльзом.

Интересно то, что Transfer-Encoding устанавливается как chunked (что в целом верно, когда длина неизвестна).

Суть в том, что это, похоже, является проявлением выбора AFNetworking использовать setHTTPBodyStream, а не setHTTPBody (который не страдает от этого поведения), что в сочетании с NSURLSession приводит к такому поведению искаженных запросов.

Я думаю, что это связано с проблемой 1398 AFNetworking.

person Rob    schedule 09.01.2014

Я сам столкнулся с этой проблемой и пробовал оба метода и предложенный здесь метод...

Оказывается, это было так же просто, как изменить ключ «имя» добавленных данных на «файл» вместо переменной имени файла.

Убедитесь, что ваш ключ данных совпадает, иначе вы увидите тот же признак пустого набора данных.

person Lytic    schedule 22.02.2015