Как правильно передать изображение методом POST в 1С без искажения данных?

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

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

Очень часто при реализации такой задачи разработчики сталкиваются с ситуацией, когда файл успешно отправляется, сервер возвращает статус «Успех», однако при попытке открыть картинку в веб-приложении она оказывается «битой» (отображается наполовину или не открывается вовсе). Мы разберем причины этой проблемы, проанализируем, почему файлы искажаются, и рассмотрим два рабочих решения: классический «обходной» метод через временные файлы и современный, быстрый подход с использованием потоков в оперативной памяти.

Выясняем причину проблемы: почему файлы «бились» при отправке?

Проанализируем типичный алгоритм, который приводит к порче бинарных данных. Изначально разработчик пытается прочитать файл картинки как обычный текст с помощью объекта ТекстовыйДокумент или ЗаписьТекста, соединить его с текстовыми заголовками multipart-запроса (такими как boundary, Content-Disposition) и записать всё это в один итоговый файл отправки.

В чем кроется главная ошибка?

Двоичные данные любого изображения (будь то JPEG, PNG или документ PDF) представляют собой последовательность сырых байтов от 0x00 до 0xFF. Они не имеют отношения к текстовым символам. Когда платформа 1С пытается прочитать картинку как текст (даже с указанием кодировок ANSI или UTF-8), происходят необратимые изменения данных:

  1. Искажение непечатных символов: Байты, которые не соответствуют ни одному валидному символу в выбранной кодировке, платформа заменяет на стандартные «символы-заполнители» (например, знаки вопроса или маркеры неопознанных символов). При записи файла обратно на диск эти байты безвозвратно теряются.
  2. Автоматическая замена переводов строк: При работе с текстовыми объектами платформа 1С автоматически нормализует окончания строк. На операционной системе Windows одиночный символ перевода строки LF (0x0A) может быть автоматически заменен парой CRLF (0x0D 0x0A), а на Linux — наоборот. В бинарном файле последовательность 0x0A — это не перевод строки, а часть заархивированных данных или цвет пикселя. Добавление или удаление даже одного байта полностью ломает структуру файла, делая его битым.

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

Решение №1: Объединение файлов на диске (классический обходной путь)

Рассмотрим первый рабочий способ, который часто использовался в версиях платформы 1С 8.2 и ранних 8.3. Его суть заключается в том, что мы не пытаемся прочитать картинку в память как текст. Вместо этого мы разделяем тело нашего HTTP-запроса на три независимых файла на диске:

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

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

Разберем пример реализации этого метода по шагам:


// Определение параметров и путей к файлам
ИмяФайлаДляЗагрузки = "C:\temp\picture.jpg"; // Наш исходный файл
ИмяФайлаОтправки = ПолучитьИмяВременногоФайла("tmp"); // Итоговый файл тела запроса

Boundary = СтрЗаменить(Строка(New УникальныйИдентификатор), "-", "");

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

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

// Если нужно передать текстовые параметры формы (например, ID пользователя)
ФайлОтправкиНачало.ЗаписатьСтроку("--" + Boundary);
ФайлОтправкиНачало.ЗаписатьСтроку("Content-Disposition: form-data; name=""user_id""");
ФайлОтправкиНачало.ЗаписатьСтроку("");
ФайлОтправкиНачало.ЗаписатьСтроку("12345");

// Заголовок для самого файла
ФайлОтправкиНачало.ЗаписатьСтроку("--" + Boundary);
ФайлОтправкиНачало.ЗаписатьСтроку("Content-Disposition: form-data; name=""image""; filename=""picture.jpg""");
ФайлОтправкиНачало.ЗаписатьСтроку("Content-Type: image/jpeg");
ФайлОтправкиНачало.ЗаписатьСтроку(""); // Важная пустая строка перед телом файла
ФайлОтправкиНачало.Закрыть();

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

// Шаг 2: Добавляем исходный неизмененный файл изображения
МассивФайловДляОбъединения.Добавить(ИмяФайлаДляЗагрузки);

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

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

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

// Шаг 5: Отправка HTTP-запроса
Соединение = Новый HTTPСоединение("api.myservice.com", , , , , 30, Новый ЗащищенноеСоединениеOpenSSL());
Запрос = Новый HTTPЗапрос("/upload");
Запрос.Заголовки.Вставить("Content-Type", "multipart/form-data; boundary=" + Boundary);

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

Попытка
    Ответ = Соединение.ОтправитьДляОбработки(Запрос);
    // Обработка ответа сервера...
Исключение
    Сообщить("Ошибка при отправке файла: " + ОписаниеОшибки());
КонецПопытки;

// Очистка временных файлов
УдалитьФайлы(ИмяФайлаОтправкиНачало);
УдалитьФайлы(ИмяФайлаОтправкиКонец);
УдалитьФайлы(ИмяФайлаОтправки);

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

Решение №2: Использование потоков в памяти (Современный стандарт)

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

Этот подход является наиболее правильным, быстрым и кроссплатформенным (одинаково эффективно работает на Windows и Linux серверах 1С). Мы создаем поток байтов прямо в оперативной памяти и последовательно записываем туда как текстовые заголовки, так и сырые бинарные данные картинки, избегая каких-либо промежуточных сохранений на диск.

Рассмотрим подробный пример реализации современного подхода:


// 1. Подготовка исходных данных
ДвоичныеДанныеКартинки = Новый ДвоичныеДанные("C:\temp\picture.jpg"); 
Boundary = СтрЗаменить(Строка(Новый УникальныйИдентификатор), "-", "");

// 2. Создаем поток в оперативной памяти
ТелоЗапроса = Новый ПотокВПамяти();

// Создаем объект для записи в поток. 
// КРИТИЧЕСКИ ВАЖНО: явно задаем разделитель строк как ВК+ПС (CRLF — Carriage Return + Line Feed).
// По стандарту HTTP разделителем строк в multipart-запросах обязан быть именно CRLF.
// Если этого не сделать, на серверах под Linux 1С запишет разделители как просто LF, и удаленный веб-сервер выдаст ошибку 400.
ЗаписьДанных = Новый ЗаписьДанных(ТелоЗапроса, КодировкаТекста.UTF8, ПорядокБайтов.LittleEndian, Символы.ВК + Символы.ПС);

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

// Записываем заголовки для передаваемого файла
ЗаписьДанных.ЗаписатьСтроку("Content-Disposition: form-data; name=""file""; filename=""picture.jpg""");
ЗаписьДанных.ЗаписатьСтроку("Content-Type: image/jpeg");
ЗаписьДанных.ЗаписатьСтроку(""); // Пустая строка отделяет заголовки от тела файла

// Записываем бинарные данные картинки байт-в-байт напрямую в поток
ЗаписьДанных.Записать(ДвоичныеДанныеКартинки);

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

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

// 4. Настройка и отправка HTTP-запроса
Соединение = Новый HTTPСоединение("api.myservice.com", , , , , 30, Новый ЗащищенноеСоединениеOpenSSL());
Запрос = Новый HTTPЗапрос("/upload");
Запрос.Заголовки.Вставить("Content-Type", "multipart/form-data; boundary=" + Boundary);

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

Попытка
    Ответ = Соединение.ОтправитьДляОбработки(Запрос);
    Сообщить("Файл успешно отправлен! Код ответа: " + Ответ.КодСостояния);
Исключение
    Сообщить("Ошибка отправки запроса: " + ОписаниеОшибки());
КонецПопытки;

Разбираемся с дополнительными параметрами формы

Часто веб-сервисы требуют передавать вместе с файлом сопутствующие текстовые параметры — например, токен авторизации, идентификатор папки, имя автора и т.д. В формате multipart/form-data каждый такой параметр представляет собой отдельную мини-секцию внутри тела запроса, отделенную строкой boundary.

Давайте посмотрим, как красиво добавить дополнительные параметры в наш поток данных перед отправкой файла:


// Предположим, у нас есть Соответствие с параметрами, которые нужно передать
ПараметрыФормы = Новый Соответствие;
ПараметрыФормы.Вставить("user_id", "98765");
ПараметрыФормы.Вставить("description", "Загрузка фото из 1С");

// Формируем текстовые параметры в потоке
Для Каждого КлючЗначение Из ПараметрыФормы Цикл
    ЗаписьДанных.ЗаписатьСтроку("--" + Boundary);
    ЗаписьДанных.ЗаписатьСтроку("Content-Disposition: form-data; name=""" + КлючЗначение.Ключ + """");
    ЗаписьДанных.ЗаписатьСтроку(""); // Обязательная пустая строка
    ЗаписьДанных.ЗаписатьСтроку(КлючЗначение.Значение);
КонецЦикла;

// И только после этого переходим к блоку отправки файла (как в примере выше)
ЗаписьДанных.ЗаписатьСтроку("--" + Boundary);
ЗаписьДанных.ЗаписатьСтроку("Content-Disposition: form-data; name=""file""; filename=""photo.jpg""");
// ... дальнейший код записи файла и закрытия потока ...

Подведем итоги

Мы подробно проанализировали причины порчи бинарных файлов при HTTP-интеграции и изучили способы решения этой проблемы. Резюмируем ключевые правила работы с файлами в HTTP-запросах 1С:

Используйте предложенные шаблоны в своих проектах, создавайте надежные интеграции и пишите качественный код!

← На главную