При интеграции 1С с внешними веб-сервисами и сайтами разработчики часто сталкиваются с задачей передачи файлов через HTTP-запрос методом POST в формате multipart/form-data. Нередко возникает ситуация, когда файлы одного типа (например, PDF) загружаются на веб-сервер без ошибок, а при передаче документов Word (.doc, .docx) или таблиц Excel (.xls, .xlsx) сервер возвращает ответ 500 Internal Server Error.
Разберем причины такого поведения веб-серверов и рассмотрим по шагам, как правильно формировать тело составного HTTP-запроса в среде 1С:Предприятие.
Проанализируем типовую ситуацию. Исходный код формирования составного запроса через временные файлы выглядит следующим образом:
ИмяФайлаОтправки = ПолучитьИмяВременногоФайла("txt");
Boundary = СтрЗаменить(Строка(Новый УникальныйИдентификатор()), "-", "");
МассивФайловДляОбъединения = Новый Массив;
ИмяФайлаОтправкиНачало = ПолучитьИмяВременногоФайла("txt");
ФайлОтправкиНачало = Новый ЗаписьТекста(ИмяФайлаОтправкиНачало, КодировкаТекста.ANSI, Символы.ПС, Ложь);
ФайлОтправкиНачало.ЗаписатьСтроку("");
ФайлОтправкиНачало.ЗаписатьСтроку("------" + Boundary);
ФайлОтправкиНачало.ЗаписатьСтроку("Content-Disposition: form-data; name=""file""; filename=""" + ИмяФайла + """");
ФайлОтправкиНачало.ЗаписатьСтроку("");
ФайлОтправкиНачало.Закрыть();
МассивФайловДляОбъединения.Добавить(ИмяФайлаОтправкиНачало);
ИмяФайлаДляЗагрузкиВременный = ПолучитьИмяВременногоФайла("tmp");
КопироватьФайл(ИмяФайлаДанных, ИмяФайлаДляЗагрузкиВременный);
МассивФайловДляОбъединения.Добавить(ИмяФайлаДляЗагрузкиВременный);
ИмяФайлаОтправкиКонец = ПолучитьИмяВременногоФайла("txt");
ФайлОтправкиКонец = Новый ЗаписьТекста(ИмяФайлаОтправкиКонец, КодировкаТекста.ANSI, Символы.ПС, Ложь);
ФайлОтправкиКонец.ЗаписатьСтроку("");
ФайлОтправкиКонец.ЗаписатьСтроку("------" + Boundary);
ФайлОтправкиКонец.ЗаписатьСтроку("Content-Disposition: form-data; name=""target""" + Символы.ПС);
ФайлОтправкиКонец.ЗаписатьСтроку(СтруктураТипаДанных.Назначение);
ФайлОтправкиКонец.ЗаписатьСтроку("------" + Boundary + "--");
ФайлОтправкиКонец.Закрыть();
МассивФайловДляОбъединения.Добавить(ИмяФайлаОтправкиКонец);
ОбъединитьФайлы(МассивФайловДляОбъединения, ИмяФайлаОтправки);
Выясним главные причины сбоя парсера на стороне веб-сервера:
filename: При записи текста с параметром КодировкаТекста.ANSI русскоязычное имя файла попадает в заголовок в кодировке Windows-1251. Веб-серверы (Nginx, Apache) и серверные обработчики при разборе заголовков multipart/form-data ожидают кодировку UTF-8 либо исключительно символы US-ASCII. Встретив некорректные байты, парсер аварийно завершает работу с кодом 500.\r\n, в 1С это Символы.ВК + Символы.ПС). Использование только Символы.ПС (LF) нарушает спецификацию RFC.Content-Type: multipart/form-data; boundary=... объявляется строка границы. В теле запроса перед этой строкой должно стоять ровно два дефиса --, а в финальной границе — два дефиса до и два дефиса после: --Boundary--. Лишние дефисы в теле запроса приводят к тому, что парсер не видит разделитель.Content-Type для бинарной части: Если заголовок с типом MIME не передан, веб-сервер может попытаться интерпретировать бинарный офисный документ как текстовый поток, что вызывает ошибку валидации.Самое простое и быстрое решение — передавать в параметре filename только латинские символы без пробелов и спецсимволов. Если необходимо сохранить оригинальное русскоязычное имя, следует использовать стандарт RFC 5987 / RFC 6266 с URL-кодированием с помощью функции КодироватьСтроку.
Посмотрим на пример правильного формирования параметров заголовка:
// Вариант 1: Приведение имени к латинице (транслитерация или безопасное имя)
ИмяФайлаБезопасное = "document.doc";
СтрокаЗаголовка = "Content-Disposition: form-data; name=""file""; filename=""" + ИмяФайлаБезопасное + """";
// Вариант 2: Передача оригинального имени по стандарту RFC 5987
ИмяФайлаURL = КодироватьСтроку(ИмяФайла, СпособКодированияСтроки.КодировкаURL);
СтрокаЗаголовка = "Content-Disposition: form-data; name=""file""; filename=""" + ИмяФайлаБезопасное + """; filename*=UTF-8''" + ИмяФайлаURL;
Обязательно передадим серверу точный заголовок Content-Type для передаваемой части файла. Рассмотрим соответствие расширений и MIME-типов:
.doc): application/msword.docx): application/vnd.openxmlformats-officedocument.wordprocessingml.document.xls): application/vnd.ms-excel.xlsx): application/vnd.openxmlformats-officedocument.spreadsheetml.sheetapplication/octet-streamВместо создания множества файлов на диске и вызова метода ОбъединитьФайлы, в современных версиях платформы 1С:Предприятие рекомендуется собирать тело запроса целиком в оперативной памяти с помощью объектов ПотокВПамяти, ЗаписьДанных и ЧтениеДанных.
Разберем по шагам готовый пример функции для сборки запроса:
Функция СформироватьТелоЗапроса(ИмяФайлаДанных, ИмяФайлаДляСервера, ЗначениеTarget, Boundary)
CRLF = Символы.ВК + Символы.ПС;
Поток = Новый ПотокВПамяти;
Запись = Новый ЗаписьДанных(Поток, КодировкаТекста.UTF8);
// 1. Формируем часть с файлом
Запись.ЗаписатьСтроку("--" + Boundary + CRLF);
// Формируем заголовок с транслитерированным/латинским именем или стандартом UTF-8
ИмяФайлаURL = КодироватьСтроку(ИмяФайлаДляСервера, СпособКодированияСтроки.КодировкаURL);
Запись.ЗаписатьСтроку("Content-Disposition: form-data; name=""file""; filename=""file.bin""; filename*=UTF-8''" + ИмяФайлаURL + CRLF);
Запись.ЗаписатьСтроку("Content-Type: application/octet-stream" + CRLF);
Запись.ЗаписатьСтроку(CRLF);
Запись.СброситьБуферы();
// 2. Записываем бинарное содержимое файла в поток
ФайлЧтение = Новый ЧтениеДанных(Новый ФайловыйПоток(ИмяФайлаДанных, РежимОткрытияФайла.Открыть));
Запись.КопироватьИз(ФайлЧтение);
ФайлЧтение.Закрыть();
// 3. Формируем текстовые поля формы
Запись.ЗаписатьСтроку(CRLF + "--" + Boundary + CRLF);
Запись.ЗаписатьСтроку("Content-Disposition: form-data; name=""target""" + CRLF);
Запись.ЗаписатьСтроку(CRLF);
Запись.ЗаписатьСтроку(ЗначениеTarget);
// 4. Финальная закрывающая граница
Запись.ЗаписатьСтроку(CRLF + "--" + Boundary + "--" + CRLF);
Запись.СброситьБуферы();
// Получаем двоичные данные собранного запроса
Результат = Поток.ЗакрытьИПолучитьДвоичныеДанные();
Возврат Результат;
КонецФункции
Рассмотрим отправку сформированных данных через HTTPСоединение:
Boundary = "----1CBoundary" + СтрЗаменить(Строка(Новый УникальныйИдентификатор()), "-", "");
ТелоЗапроса = СформироватьТелоЗапроса("C:\temp\report.doc", "Отчет.doc", "UploadTarget", Boundary);
Заголовки = Новый Соответствие;
Заголовки.Вставить("Content-Type", "multipart/form-data; boundary=" + Boundary);
HTTPЗапрос = Новый HTTPЗапрос("/api/upload", Заголовки);
HTTPЗапрос.УстановитьТелоИзДвоичныхДанных(ТелоЗапроса);
Соединение = Новый HTTPСоединение("example.com", 443, , , , 30, Новый ЗащищенноеСоединениеOpenSSL());
HTTPОтвет = Соединение.ОтправитьДляОбработки(HTTPЗапрос);
Использование правильных переводов строк CRLF, корректного формата границ --Boundary и безопасных латинских имен файлов с URL-кодированием позволяет полностью устранить ошибку 500 при выгрузке любых офисных документов из 1С.