Приветствуем, коллеги! Сегодня мы детально разберем одну из самых распространенных и коварных проблем, с которой сталкиваются разработчики 1С при интеграции с веб-сервисами — отправку изображений и других бинарных файлов с помощью HTTP-запроса POST (в формате multipart/form-data) без использования кодирования в Base64.
Очень часто при реализации такой задачи разработчики сталкиваются с ситуацией, когда файл успешно отправляется, сервер возвращает статус «Успех», однако при попытке открыть картинку в веб-приложении она оказывается «битой» (отображается наполовину или не открывается вовсе). Мы разберем причины этой проблемы, проанализируем, почему файлы искажаются, и рассмотрим два рабочих решения: классический «обходной» метод через временные файлы и современный, быстрый подход с использованием потоков в оперативной памяти.
Проанализируем типичный алгоритм, который приводит к порче бинарных данных. Изначально разработчик пытается прочитать файл картинки как обычный текст с помощью объекта ТекстовыйДокумент или ЗаписьТекста, соединить его с текстовыми заголовками multipart-запроса (такими как boundary, Content-Disposition) и записать всё это в один итоговый файл отправки.
В чем кроется главная ошибка?
Двоичные данные любого изображения (будь то JPEG, PNG или документ PDF) представляют собой последовательность сырых байтов от 0x00 до 0xFF. Они не имеют отношения к текстовым символам. Когда платформа 1С пытается прочитать картинку как текст (даже с указанием кодировок ANSI или UTF-8), происходят необратимые изменения данных:
0x0A) может быть автоматически заменен парой CRLF (0x0D 0x0A), а на Linux — наоборот. В бинарном файле последовательность 0x0A — это не перевод строки, а часть заархивированных данных или цвет пикселя. Добавление или удаление даже одного байта полностью ломает структуру файла, делая его битым.Именно поэтому при сравнении исходного и полученного файлов в утилитах сравнения (например, WinDiff) видны точечные различия в строках — это результат работы текстового энкодера платформы. Чтобы передать файл без изменений, мы должны работать с ним исключительно как с двоичными данными.
Рассмотрим первый рабочий способ, который часто использовался в версиях платформы 1С 8.2 и ранних 8.3. Его суть заключается в том, что мы не пытаемся прочитать картинку в память как текст. Вместо этого мы разделяем тело нашего HTTP-запроса на три независимых файла на диске:
boundary, текстовые параметры запроса и заголовки для передаваемого файла (Content-Disposition, Content-Type).После подготовки этих трех файлов мы склеиваем их в один итоговый файл тела запроса с помощью встроенного метода платформы ОбъединитьФайлы(). Этот метод производит побайтовое копирование, благодаря чему бинарные данные картинки не искажаются.
Разберем пример реализации этого метода по шагам:
// Определение параметров и путей к файлам
ИмяФайлаДляЗагрузки = "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 операции). В многопоточных высоконагруженных системах это может приводить к снижению производительности и конфликтам блокировок файлов операционной системой.
Начиная с версии платформы 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С:
ТекстовыйДокумент, ЗаписьТекста) для чтения или изменения бинарных файлов (изображения, PDF, архивы).ОбъединитьФайлы().ПотокВПамяти и ЗаписьДанных. Это быстрее, безопаснее и экономит ресурсы сервера.Символы.ВК + Символы.ПС при инициализации ЗаписьДанных для корректной работы HTTP-разметки на Linux-серверах.Используйте предложенные шаблоны в своих проектах, создавайте надежные интеграции и пишите качественный код!