Как исправить мерцающую ошибку при отправке PDF-файлов через HTTP в 1С

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При разработке механизмов интеграции 1С с внешними веб-сервисами программисты часто сталкиваются с ситуацией, когда текстовые файлы или небольшие изображения передаются без проблем, но при попытке отправить PDF-скан договора возникает «мерцающая» ошибка. Сервер либо возвращает ошибку 400 (Bad Request), либо сообщает, что файл поврежден или не загружен. При этом тот же самый файл успешно уходит через Postman или после предварительной архивации.

В этой статье мы подробно разберем, почему 1С может некорректно формировать тело HTTP-запроса для бинарных данных, рассмотрим влияние Byte Order Mark (BOM), символов переноса строк и внутренней структуры PDF-файлов на результат выгрузки.

Анализ исходной ситуации

Рассмотрим типичный код, который используется для ручного формирования сообщения multipart/form-data. Проблема часто кроется в функции сборки тела запроса, где бинарные данные файла смешиваются с текстовыми разделителями (boundary).


Функция СоздатьСообщение_Изображение(ИмяСообщения, ИмяФайла, Картинка, Разделитель)       
    Поток = Новый ПотокВПамяти();
    ЗаписьДанных = Новый ЗаписьДанных(Поток);
    
    // Формирование заголовка части (Part)
    ЗаписьДанных.ЗаписатьСтроку("--" + Разделитель); 
    ЗаписьДанных.ЗаписатьСтроку("Content-Disposition: form-data; name=""file""; filename=""1.pdf""");
    ЗаписьДанных.ЗаписатьСтроку("");
    
    // Запись бинарных данных файла
    ЗаписьДанных.Записать(Новый ДвоичныеДанные(Картинка));
    ЗаписьДанных.Закрыть();
    
    Возврат Поток.ЗакрытьИПолучитьДвоичныеДанные();
КонецФункции

На первый взгляд код выглядит корректным, но именно в нем заложены причины нестабильной работы. Разберем их по порядку.

Причина 1: Невидимые символы BOM (Byte Order Mark)

Одной из самых коварных особенностей объекта ЗаписьДанных в 1С является автоматическое добавление маркера последовательности байтов (BOM) при использовании кодировки UTF-8. Если при создании объекта ЗаписьДанных явно не указать параметры, система может вставить в начало потока несколько байтов (EF BB BF), которые сервер воспримет как часть данных или мусор.

Проанализируем, как правильно инициализировать запись, чтобы избежать порчи PDF-файла. Нам необходимо использовать конструктор с явным отключением BOM:


// Правильная инициализация без использования BOM
ЗаписьДанных = Новый ЗаписьДанных(Поток, КодировкаТекста.UTF8, ДопустимыйBOM.НеИспользовать);

Если этого не сделать, первая же команда ЗаписатьСтроку добавит BOM в начало потока. Для PDF-файлов, которые крайне чувствительны к лишним байтам в структуре, это может стать критичным, особенно если сервер строго валидирует заголовок файла.

Причина 2: Символы переноса строк (CRLF vs LF)

Согласно спецификации протокола HTTP, разделители в multipart/form-data должны отделяться друг от друга строго парой символов \r\n (CRLF). Метод ЗаписатьСтроку в 1С по умолчанию использует системный символ переноса строки. Если сервер работает на ОС Linux и ожидает стандартные заголовки, а 1С передает данные в специфическом формате, разбор пакета на стороне сервера может завершиться ошибкой.

Рекомендуется явно указывать символы переноса строк при формировании заголовков внутри тела запроса. Вместо стандартного метода можно использовать запись явных последовательностей байтов или убедиться, что настройки ЗаписьДанных соответствуют стандартам HTTP.

Причина 3: Отсутствие внутреннего заголовка Content-Type

Выясним еще одну важную деталь. В примере кода выше для части с файлом указывается только Content-Disposition. Однако многие современные API требуют, чтобы для каждой части был указан ее собственный Content-Type. Для PDF-файлов это application/pdf.

Рассмотрим, как должна выглядеть структура заголовков внутри функции:


ЗаписьДанных.ЗаписатьСтроку("--" + Разделитель); 
ЗаписьДанных.ЗаписатьСтроку("Content-Disposition: form-data; name=""file""; filename=""contract.pdf""");
ЗаписьДанных.ЗаписатьСтроку("Content-Type: application/pdf"); // Обязательно добавьте эту строку
ЗаписьДанных.ЗаписатьСтроку(""); // Пустая строка перед данными

Без этого заголовка сервер может попытаться интерпретировать бинарное содержимое PDF как текст (например, в кодировке ASCII), что приведет к искажению данных при наличии в файле специфических байтовых последовательностей.

Причина 4: Специфика PDF-структуры и «хвосты» файлов

Проанализируем ситуацию, когда файл «лечится» пересохранением или редактированием метаданных (например, через BecyPDFMetaEdit). Сканеры часто генерируют PDF с ошибками в таблице перекрестных ссылок (xref) или с мусорными байтами после финального тега %%EOF.

При ручной сборке запроса в 1С, если мы просто записываем ДвоичныеДанные файла, а затем сразу пишем строку закрывающего разделителя (boundary), эти «мусорные» байты могут слиться с разделителем. В итоге сервер не сможет найти границу файла и выдаст ошибку.

Посмотрим на структуру завершения запроса. Важно, чтобы между данными файла и финальным разделителем был четкий перевод строки:


// Запись данных файла
ЗаписьДанных.Записать(ДанныеФайла);
// Обязательный переход на новую строку перед финальным boundary
ЗаписьДанных.ЗаписатьСтроку(""); 
ЗаписьДанных.ЗаписатьСтроку("--" + Разделитель + "--");

Рекомендованный способ реализации (Методика ИТС)

Чтобы избежать «изобретения велосипеда» и минимизировать ошибки, лучше всего использовать проверенный алгоритм формирования сообщения. Рассмотрим вариант, который минимизирует риск порчи бинарных данных за счет правильной работы с потоками.


Функция СформироватьТелоЗапроса(ДанныеФайла, ИмяФайла, Разделитель)
    Результат = Новый ПотокВПамяти();
    ЗаписьДанных = Новый ЗаписьДанных(Результат, КодировкаТекста.UTF8, ДопустимыйBOM.НеИспользовать);
    
    // Начало блока файла
    ЗаписьДанных.ЗаписатьСтроку("--" + Разделитель);
    ЗаписьДанных.ЗаписатьСтроку("Content-Disposition: form-data; name=""file""; filename=""" + ИмяФайла + """");
    ЗаписьДанных.ЗаписатьСтроку("Content-Type: application/pdf");
    ЗаписьДанных.ЗаписатьСтроку("");
    
    // Выталкиваем буфер в поток перед записью двоичных данных
    ЗаписьДанных.СброситьБуфер();
    
    // Записываем сам файл напрямую в поток
    Результат.Записать(ДанныеФайла);
    
    // Завершаем блок
    ЗаписьДанных.ЗаписатьСтроку("");
    ЗаписьДанных.ЗаписатьСтроку("--" + Разделитель + "--");
    ЗаписьДанных.Закрыть();
    
    Возврат Результат.ЗакрытьИПолучитьДвоичныеДанные();
КонецФункции

Важный момент: Использование метода СброситьБуфер() гарантирует, что текстовые заголовки уже попали в целевой поток до того, как вы начнете писать туда «сырые» байты файла. Это предотвращает перемешивание данных в буфере записи.

Почему помогает архивация?

Выясним, почему упаковка в ZIP решает проблему. Файловый формат ZIP по своей природе не содержит текстовых последовательностей, которые могли бы конфликтовать с логикой обработки строк. Кроме того, архиватор «нормализует» данные, и любые проблемные хвосты PDF-файла становятся просто частью сжатого потока, который сервер гарантированно воспринимает как бинарный блок целиком.

Итоги и рекомендации

Подводя итог, если вы столкнулись с ошибкой при передаче файлов через HTTPСоединение, выполните следующие шаги:

  1. Проверьте, не добавляется ли BOM в начало запроса (используйте ДопустимыйBOM.НеИспользовать).
  2. Явно укажите внутренний заголовок Content-Type: application/pdf для части сообщения, содержащей файл.
  3. Убедитесь, что между бинарными данными файла и текстовым разделителем (boundary) есть корректный перевод строки.
  4. Если структура PDF-файла от сканера некорректна, рассмотрите возможность использования объекта ЗаписьZipФайла для временной упаковки или используйте метод УстановитьТелоИзФайла() объекта HTTPЗапрос, который работает с бинарными данными более прямолинейно.

Использование правильных объектов для работы с потоками (ПотокВПамяти, ЗаписьДанных) позволяет создавать надежные механизмы обмена данными, устойчивые к особенностям различных форматов файлов.

← На главную