Как выгрузить файл POST-запросом из 1С: разбор multipart/form-data через временные файлы и потоки в памяти

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

Интеграция 1С с веб-сервисами — одна из самых частых задач современного разработчика. Особенно много вопросов возникает, когда требуется передать на сторонний сайт или в облачное хранилище бинарный файл (картинку, PDF-документ или Word) с помощью HTTP-запроса POST в формате multipart/form-data. Этот формат имитирует отправку файла пользователем через обычную форму в браузере.

В этой статье мы подробно разберем, почему стандартные попытки собрать тело запроса "в лоб" через обычную строку или кодирование в Base64 часто приводят к ошибкам сервера. Мы детально проанализируем ситуацию, выявим скрытые нюансы и изучим два рабочих способа реализации: классический (через склеивание временных файлов) и современный производительный (с использованием потоков в памяти).

В чем сложность multipart/form-data и почему Base64 не всегда подходит?

Разберем по шагам структуру запроса multipart/form-data. Когда мы отправляем файл через браузер, запрос состоит из заголовков и сложного составного тела. Каждая часть тела (параметр формы или сам файл) отделяется уникальной строкой-разделителем — Boundary.

Типичная ошибка начинающих разработчиков в 1С заключается в попытке прочитать бинарный файл, перевести его в текстовую строку Base64 с помощью функции Base64Строка() и вставить полученный текст в тело запроса. Большинство веб-серверов ожидают на входе чистые двоичные данные (сырые байты) файла, а не его текстовое представление. Если сервер не спроектирован специально под прием Base64, он просто запишет зашифрованную строку в файл на своей стороне, сделав его нечитаемым, либо сразу вернет ошибку разбора структуры.

Вторая проблема — ограничение работы со строками в 1С. Мы не можем просто прибавить двоичные данные файла к текстовой строке 1С, содержащей заголовки HTTP. При попытке преобразовать бинарный файл в строку происходит искажение спецсимволов и кодировки, из-за чего файл "бьется" при передаче.

Рассмотрим подробнее два надежных метода решения этой задачи.

Способ 1: Классический метод через временные файлы (для любых версий 1С v8)

Этот подход основан на создании нескольких временных файлов на жестком диске с последующим их физическим объединением в один общий файл-тело с помощью встроенного метода 1С ОбъединитьФайлы(). Такой файл-тело затем отправляется на сервер одной командой без преобразования бинарных данных в текст.

Идея алгоритма состоит в разделении тела запроса на три части:

  1. Начальный текстовый блок: содержит открывающую границу boundary, метаданные поля формы (Content-Disposition, имя параметра, имя передаваемого файла) и указание типа содержимого (Content-Type).
  2. Бинарный блок: непосредственно сам отправляемый файл в неизменном виде (без Base64 и перекодирования).
  3. Конечный текстовый блок: содержит закрывающую границу boundary (с добавлением дефисов в конце).

Рассмотрим пример кода, реализующий этот алгоритм:


// Определяем пути к рабочим файлам
ИмяФайлаДляЗагрузки = "C:\temp.txt"; // Файл, который нужно отправить
ИмяФайлаОтправки = КаталогВременныхФайлов() + "post_body.tmp"; 

// Создаем уникальный разделитель границ (Boundary)
Boundary = "Boundary" + СтрЗаменить(Строка(Новый УникальныйИдентификатор()), "-", "");

МассивФайловДляОбъединения = Новый Массив; 

// 1. Формируем стартовую текстовую часть
ИмяФайлаОтправкиНачало = ПолучитьИмяВременногоФайла("txt"); 
ФайлОтправкиНачало = Новый ЗаписьТекста(ИмяФайлаОтправкиНачало, КодировкаТекста.ANSI, Символы.ПС, Ложь); 

// Записываем разделитель и заголовки части формы
ФайлОтправкиНачало.ЗаписатьСтроку("--" + Boundary); 
ФайлОтправкиНачало.ЗаписатьСтроку("Content-Disposition: form-data; name=""file""; filename=""" + ИмяФайлаДляЗагрузки + """"); 
ФайлОтправкиНачало.ЗаписатьСтроку("Content-Type: application/octet-stream"); 
ФайлОтправкиНачало.ЗаписатьСтроку(""); // Пустая строка перед началом бинарных данных
ФайлОтправкиНачало.Закрыть(); 

МассивФайловДляОбъединения.Добавить(ИмяФайлаОтправкиНачало); 

// 2. Добавляем сам загружаемый файл
// Чтобы избежать конфликтов блокировки оригинального файла, скопируем его во временный
ИмяФайлаДляЗагрузкиВременный = ПолучитьИмяВременногоФайла("tmp"); 
КопироватьФайл(ИмяФайлаДляЗагрузки, ИмяФайлаДляЗагрузкиВременный); 
МассивФайловДляОбъединения.Добавить(ИмяФайлаДляЗагрузкиВременный); 

// 3. Формируем финальную текстовую часть (закрытие границ)
ИмяФайлаОтправкиКонец = ПолучитьИмяВременногоФайла("txt"); 
ФайлОтправкиКонец = Новый ЗаписьТекста(ИмяФайлаОтправкиКонец, КодировкаТекста.ANSI, Символы.ПС, Ложь); 
ФайлОтправкиКонец.ЗаписатьСтроку(""); // Перенос строки после бинарных данных
ФайлОтправкиКонец.ЗаписатьСтроку("--" + Boundary + "--"); 
ФайлОтправкиКонец.Закрыть(); 

МассивФайловДляОбъединения.Добавить(ИмяФайлаОтправкиКонец); 

// Объединяем все три файла в один файл-тело запроса
ОбъединитьФайлы(МассивФайловДляОбъединения, ИмяФайлаОтправки); 

// Подготавливаем HTTP-соединение и запрос
Сервер = "api.mysite.ru";
АдресРесурса = "/v1/upload";

Заголовки = Новый Соответствие();
Заголовки.Вставить("Content-Type", "multipart/form-data; boundary=" + Boundary);
Заголовки.Вставить("Authorization", "Bearer YOUR_TOKEN_HERE");

HTTPСоединение = Новый HTTPСоединение(Сервер, , , , , , Новый ЗащищенноеСоединениеOpenSSL());
HTTPЗапрос = Новый HTTPЗапрос(АдресРесурса, Заголовки);

// Указываем собранный файл в качестве тела запроса
HTTPЗапрос.УстановитьИмяФайлаТела(ИмяФайлаОтправки); 

// Отправляем запрос и получаем ответ
HTTPОтвет = HTTPСоединение.ОтправитьДляОбработки(HTTPЗапрос);
ТекстОтвета = HTTPОтвет.ПолучитьТелоКакСтроку();
Сообщить("Ответ сервера: " + ТекстОтвета);

// Очищаем временные файлы
Для Каждого ВременныйФайл Из МассивФайловДляОбъединения Цикл
    УдалитьФайлы(ВременныйФайл);
КонецЦикла;
УдалитьФайлы(ИмяФайлаОтправки);

Этот метод полностью рабочий и гарантированно доставит файл на сервер в исходном бинарном виде. Однако у него есть существенные недостатки: частые операции чтения-записи (I/O) на жесткий диск снижают производительность системы, а на серверных кластерах 1С под управлением разных операционных систем могут возникнуть проблемы с правами доступа к временным папкам.

Способ 2: Современный метод через потоки в памяти (рекомендуемый для платформ 8.3.10 и выше)

Начиная с версии платформы 1С:Предприятие 8.3.10, разработчикам стал доступен мощный функционал работы с потоками без сохранения данных на диск. Мы можем использовать объекты ПотокВПамяти и ЗаписьДанных. Это позволяет динамически формировать тело запроса прямо в оперативной памяти компьютера или сервера, что существенно увеличивает скорость выполнения кода и избавляет систему от генерации временных файлов-мусорщиков.

Проанализируем ситуацию подробнее. Для успешной обработки запроса большинством веб-серверов (особенно под управлением nginx, Apache или Linux-систем) крайне важно использовать правильные символы перевода строки. По стандартам RFC это должна быть строго последовательность **CRLF** (возврат каретки и перевод строки: \r\n, в 1С это сочетание Символы.ВК + Символы.ПС). Обычный символ Символы.ПС (LF) может быть отвергнут сервером, из-за чего разбор структуры тела запроса сломается.

Также критически важно отключать BOM (Byte Order Mark) при инициализации записи текстовых фрагментов, чтобы в поток не попали невидимые служебные байты, мешающие распознаванию границ разделителя.

Посмотрим на пример современного кода:


// Настройки отправляемого файла и параметров соединения
ПутьКФайлу = "C:\temp.txt";
ИмяФайла = "temp.txt";
АдресРесурса = "/v1/upload";
Сервер = "api.mysite.ru";

// Создаем уникальный разделитель границ
Boundary = "Boundary" + СтрЗаменить(Строка(Новый УникальныйИдентификатор()), "-", "");
РазделительСтрок = Символы.ВК + Символы.ПС; // Строго CRLF по стандарту RFC

// Инициализируем поток в оперативной памяти
ТелоЗапроса = Новый ПотокВПамяти();

// ЗаписьДанных настраиваем на UTF-8 БЕЗ использования BOM (последний параметр - Ложь)
ЗаписьДанных = Новый ЗаписьДанных(ТелоЗапроса, КодировкаТекста.UTF8, ПорядокБайтов.LittleEndian, "", "", Ложь);

// 1. Записываем стартовую границу
ЗаписьДанных.ЗаписатьСтроку("--" + Boundary + РазделительСтрок);

// 2. Пишем заголовки части файла
ЗаписьДанных.ЗаписатьСтроку("Content-Disposition: form-data; name=""file""; filename=""" + ИмяФайла + """" + РазделительСтрок);
ЗаписьДанных.ЗаписатьСтроку("Content-Type: application/octet-stream" + РазделительСтрок);
ЗаписьДанных.ЗаписатьСтроку(РазделительСтрок); // Пустая строка отделяет заголовки от тела данных

// Фиксируем текстовые заголовки в потоке
ЗаписьДанных.Флаш(); 

// 3. Считываем бинарные данные исходного файла и пишем напрямую в поток
ДвоичныеДанныеФайла = Новый ДвоичныеДанные(ПутьКФайлу);
ЗаписьДанных.Записать(ДвоичныеДанныеФайла);

// 4. Записываем закрывающую границу
ЗаписьДанных.ЗаписатьСтроку(РазделительСтрок);
ЗаписьДанных.ЗаписатьСтроку("--" + Boundary + "--" + РазделительСтрок);

// Закрываем объект записи, данные остаются в потоке памяти
ЗаписьДанных.Закрыть();

// Получаем сформированные двоичные данные из потока памяти
ДанныеДляОтправки = ТелоЗапроса.ЗакрытьИПолучитьДвоичныеДанные();

// Готовим запрос
Заголовки = Новый Соответствие();
Заголовки.Вставить("Content-Type", "multipart/form-data; boundary=" + Boundary);
Заголовки.Вставить("Authorization", "Bearer YOUR_TOKEN_HERE");

HTTPСоединение = Новый HTTPСоединение(Сервер, , , , , , Новый ЗащищенноеСоединениеOpenSSL());
HTTPЗапрос = Новый HTTPЗапрос(АдресРесурса, Заголовки);

// Передаем двоичные данные в HTTP-запрос
HTTPЗапрос.УстановитьТелоИзДвоичныхДанных(ДанныеДляОтправки);

// Выполняем отправку
HTTPОтвет = HTTPСоединение.ОтправитьДляОбработки(HTTPЗапрос);
ТекстОтвета = HTTPОтвет.ПолучитьТелоКакСтроку();
Сообщить("Ответ сервера: " + ТекстОтвета);

Преимущества использования потоков в памяти:

Таким образом, мы выяснили причины частых ошибок при формировании составных POST-запросов и рассмотрели два надежных варианта реализации загрузки файлов на сайт. Способ с использованием ПотокВПамяти и объекта ЗаписьДанных является на сегодняшний день наиболее современным, надежным и предпочтительным стандартом разработки в 1С:Предприятии.

← На главную