Загрузка файла составной формы — Загруженный файл иногда имеет дополнительный разрыв строки в конце файла

#c# #multipartform-data

Вопрос:

У меня довольно сумасшедшая проблема, и я надеюсь, что кто-нибудь может дать мне несколько советов о том, как ее решить.

На стороне клиента я загружаю составную форму на сервер с помощью C #. В форме есть поле для хэша MD5 и поле для аудиофайла.

Проблема в том, что иногда случается, что загруженный аудиофайл имеет разрыв строки в конце файла, которого нет в исходном файле на стороне клиента. И это, конечно же, приводит к сбою проверки хэша MD5.

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

Вот код из моего класса HttpMultipartFormSender:

 public class HttpMultipartFormSender
{
    private HttpRequestMessage _requestMessage;

    public HttpMultipartFormSender(string requestUri)
    {
        UploadProgressHandler = new ProgressMessageHandler();
        var boundary = "----------"   DateTime.Now.Ticks.ToString("x", CultureInfo.InvariantCulture);
        Content = new MultipartFormDataContent(boundary);
        _requestMessage = new HttpRequestMessage(HttpMethod.Post, requestUri);
    }

    public MultipartFormDataContent Content { get; set; }

    public ProgressMessageHandler UploadProgressHandler { get; }

    public async Task<bool> SendFormAsync()
    {
        _requestMessage.Content = Content;
        var client = HttpClientFactory.Create(UploadProgressHandler);
        client.Timeout = TimeSpan.FromMinutes(30);

        var httpResponse = await client.SendAsync(_requestMessage);
        if (!httpResponse.IsSuccessStatusCode)
        {
            throw new InvalidOperationException("Exception thrown while file upload");
        }

        return true;
    }

    #endregion
}
 

И использование выглядит так:

 VmPropUserMessage = $"File: {Path.GetFileName(filePath)} is uploading...";
await using var fileStream =
    new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);

var md5StringContent = new StringContent(GetFileMd5(filePath));
var fileStreamContent = new StreamContent(fileStream);
fileStreamContent.Headers.Add("Content-Type", "application/octet-stream");

var formSender       = new HttpMultipartFormSender($"{VmPropSelectedTranscoder.Url}/upload");
formSender.UploadProgressHandler.HttpSendProgress  = FileUploadProgressChanged;
formSender.Content.Add(md5StringContent, "file_hash");
formSender.Content.Add(fileStreamContent, "file", filePath);
await formSender.SendFormAsync();
 

На стороне сервера я использую этот код, вырезанный для хранения потока из запроса:

 private static void SaveFile(TranscodingJobModel transcodingJob, Stream requestFileContent)
{
    var uploadedFilePath = $"{AppConfig.AudioUploadDir}\{transcodingJob.GetTranscodingJobTmpFileName}";

    using (var fileSaveStream = File.Create(uploadedFilePath))
    {
        requestFileContent.CopyTo(fileSaveStream);
        fileSaveStream.Flush(true);
        fileSaveStream.Close();
        requestFileContent.Close();
    }
}
 

Иногда у меня бывает такой результат:

На стороне клиента файл заканчивается только одной новой строкой:

На стороне источника

На стороне сервера файл заканчивается двумя новыми строками:

На стороне назначения

У кого-нибудь есть идея, почему это происходит?

Заранее спасибо

Комментарии:

1. Просто любопытно, можете ли вы воспроизвести проблему, т. Е. Есть ли у вас «неисправный» файл, который всегда приводит к возникновению этой проблемы, или это просто случайное одноразовое событие?

2. @Culme Да, я могу воспроизвести с неисправным файлом.

3. Действительно ли размер файла увеличивается на 1 байт после загрузки, если вы сравниваете файл до и после?

Ответ №1:

Спецификация составного содержимого содержит некоторые конкретные синтаксические требования в отношении CRLF/новых строк:

https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html

Обратите внимание, что граница инкапсуляции должна находиться в начале строки, т. Е. после CRLF, и что эта начальная CRLF считается частью границы инкапсуляции, а не частью предыдущей части. За границей должна немедленно следовать либо другая CRLF и поля заголовка для следующей части, либо две CRLF, в этом случае полей заголовка для следующей части нет (и поэтому предполагается, что они имеют тип содержимого text/plain). ПРИМЕЧАНИЕ: CRLF, предшествующий строке инкапсуляции, считается частью границы, так что можно иметь часть, которая не заканчивается CRLF (разрыв строки). Части тела, которые должны рассматриваться как заканчивающиеся разрывами строк, поэтому должны иметь два CRLF, предшествующих строке инкапсуляции, первая из которых является частью предыдущей части тела, а вторая-частью границы инкапсуляции.

Требование, чтобы граница инкапсуляции начиналась с CRLF, подразумевает, что тело составного объекта само должно начинаться с CRLF перед первой строкой инкапсуляции-то есть, если область «преамбула» не используется, за заголовками сущности должны следовать ДВА CRLF. Именно так и должны быть составлены такие сущности. Однако толерантная программа чтения почты может интерпретировать тело типа multipart, которое начинается со строки инкапсуляции, НЕ инициированной CRLF, как также являющуюся границей инкапсуляции, но совместимая программа отправки почты не должна генерировать такие сущности.

Тип MultipartFormDataContent наследует поведение от MultipartContent, где базовый источник помогает проиллюстрировать, как это делается .NET обрабатывает применение этой спецификации при объединении нескольких базовых элементов контента:

Я поднимаю этот вопрос, потому что часть спецификации включает в себя добавление новых строк в рамках сегментации контента, и это может дать вам возможность написать модульный тест на клиенте с вашим файлом-нарушителем, чтобы сгенерировать составную часть и оценить ее на нескольких этапах, чтобы определить, в чем заключается проблема и связана ли эта проблема с кодом создания контента клиента или кодом потребления сервера. Если вы проверите содержимое файла до того, как составное содержимое будет обработано в поток, будет ли оно по-прежнему совпадать? Если вы обрабатываете составное содержимое в поток и считываете этот поток обратно локально (без отправки по проводу), совпадает ли он по-прежнему?