Интеграция 1С с веб-сервисами — одна из самых частых задач современного разработчика. Особенно много вопросов возникает, когда требуется передать на сторонний сайт или в облачное хранилище бинарный файл (картинку, PDF-документ или Word) с помощью HTTP-запроса POST в формате multipart/form-data. Этот формат имитирует отправку файла пользователем через обычную форму в браузере.
В этой статье мы подробно разберем, почему стандартные попытки собрать тело запроса "в лоб" через обычную строку или кодирование в Base64 часто приводят к ошибкам сервера. Мы детально проанализируем ситуацию, выявим скрытые нюансы и изучим два рабочих способа реализации: классический (через склеивание временных файлов) и современный производительный (с использованием потоков в памяти).
Разберем по шагам структуру запроса multipart/form-data. Когда мы отправляем файл через браузер, запрос состоит из заголовков и сложного составного тела. Каждая часть тела (параметр формы или сам файл) отделяется уникальной строкой-разделителем — Boundary.
Типичная ошибка начинающих разработчиков в 1С заключается в попытке прочитать бинарный файл, перевести его в текстовую строку Base64 с помощью функции Base64Строка() и вставить полученный текст в тело запроса. Большинство веб-серверов ожидают на входе чистые двоичные данные (сырые байты) файла, а не его текстовое представление. Если сервер не спроектирован специально под прием Base64, он просто запишет зашифрованную строку в файл на своей стороне, сделав его нечитаемым, либо сразу вернет ошибку разбора структуры.
Вторая проблема — ограничение работы со строками в 1С. Мы не можем просто прибавить двоичные данные файла к текстовой строке 1С, содержащей заголовки HTTP. При попытке преобразовать бинарный файл в строку происходит искажение спецсимволов и кодировки, из-за чего файл "бьется" при передаче.
Рассмотрим подробнее два надежных метода решения этой задачи.
Этот подход основан на создании нескольких временных файлов на жестком диске с последующим их физическим объединением в один общий файл-тело с помощью встроенного метода 1С ОбъединитьФайлы(). Такой файл-тело затем отправляется на сервер одной командой без преобразования бинарных данных в текст.
Идея алгоритма состоит в разделении тела запроса на три части:
Рассмотрим пример кода, реализующий этот алгоритм:
// Определяем пути к рабочим файлам
ИмяФайлаДляЗагрузки = "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С под управлением разных операционных систем могут возникнуть проблемы с правами доступа к временным папкам.
Начиная с версии платформы 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С:Предприятии.