Как передать по http одним POST запросом файл (двоичные данные) и JSON?
Файл передается и принимается на сервере нормально, а массив нет. Когда смотрю запрос в DevTools вижу это:
Form Data: file: (binary) text: (unable to decode value)
Вот запрос:
fetch('/api/newPost', < method: "POST", body: formData >)
Отслеживать
задан 18 окт 2019 в 20:10
Dmitry Filippov Dmitry Filippov
623 5 5 серебряных знаков 24 24 бронзовых знака
1 ответ 1
Сортировка: Сброс на вариант по умолчанию
Перед передачей преобразуйте массив в json-строку. Передавать форму надо как multipart/form-data , а потом на сервере обрабатывать каждое поле отдельно.
let formData = new FormData(); formData.append('file', blob); formData.append('text', JSON.stringify(['dima', 'sasha'])); fetch('/api/newPost', < method: "POST", headers: < "Content-Type": "multipart/form-data" >, body: formData >)
Отслеживать
ответ дан 18 окт 2019 в 20:46
Dmitry Kozlov Dmitry Kozlov
6,708 2 2 золотых знака 15 15 серебряных знаков 30 30 бронзовых знаков
POST запрос, составное содержимое (multipart/form-data)

В жизни любого программиста попадаются задачки, которые человека цепляют. Вот не нравится стандартный метод решения и все! А порой бывает, что стандартные решения не подходят по какой-то причине. Некоторые люди обходят такие задачи стороной, другие же любят решать их. Можно даже сказать сами их находят. Одна из таких задач отсылка файла или несколько файлов методом POST.
Некоторые наверное скажут, эта задача совсем не задача. Ведь есть замечательная библиотека CURL, которая довольно простая и решает эту задачу легко! Но не спешите. Да, CURL мощная библиотека, да она загружает файлы, но… Как Вы знаете у нее есть маленькая особенность — файл должен быть размещен на жестком диске!
А теперь давайте представим себе такую ситуацию, Вы генерируете динамически файл или же он уже находится в памяти и нужно его отправить методом POST на удаленный Web сервер. Что же тогда получается? Перед его отправкой нужно его сохранить? Да именно так и поступило бы 90% программистов. Зачем искать лишние проблемы, если решение лежит на поверхности? Но мы же с Вами не из этих 90%! Мы же лучше, мы же можем решить любую задачку. Зачем нам лишнее действие? Во-первых, оно задействует не быструю файловую систему жесткого диска. Во-вторых, у нас может и не быть доступа к файловой системе или же там выделено слишком мало места.
Как же нам тогда решить эту задачку? Для этого надо взглянуть как собственно передаются данные методом POST. Единственный вариант решения — это передача файла составным запросом с помощью multipart/form-data. Этот метод хорошо описан в RFC7578. Давайте взглянем как будет выглядеть тело POST запроса multipart/form-data:
POST /form.html HTTP/1.1 Host: server.com Referer: http://server.com/form.html User-Agent: Mozilla Content-Type: multipart/form-data; boundary=-------------573cf973d5228 Content-Length: 288 Connection: keep-alive Keep-Alive: 300 (пустая строка) (отсутствующая преамбула) ---------------573cf973d5228 Content-Disposition: form-data; name="field" text ---------------573cf973d5228 Content-Disposition: form-data; name="file"; filename="sample.txt" Content-Type: text/plain Content file ---------------573cf973d5228--
Наше тело состоит из двух частей, в первой части мы передаем значение поля формы name=«field» равное: text. Во второй части мы передаем поле name=«file» с содержимым файла filename=«sample.txt»: Content file. В заголовке мы указываем формат содержимого POST запроса — Content-Type: multipart/form-data, строку разделитель составных частей: boundary=————-573cf973d5228 и длину сообщения — Content-Length: 288.
Осталось, собственно, написать программу реализующий этот метод. Так как мы люди умные и не пишем по сто раз одно и тоже в разных проектах, то оформим все в виде класса реализующий этот метод. Плюс к этому, расширим его для разных вариантов отправки как файлов, так и простых элементов формы. А что бы отличить среди массива POST данных, наличие файла, создадим отдельный файл — контейнер с содержимым файла и его данных (имя и расширение). Таким образом он будет выглядеть следующим образом:
class oFile < private $name; private $mime; private $content; public function __construct($name, $mime=null, $content=null) < // Проверяем, если $content=null, значит в переменной $name - путь к файлу if(is_null($content)) < // Получаем информацию по файлу (путь, имя и расширение файла) $info = pathinfo($name); // проверяем содержится ли в строке имя файла и можно ли прочитать файл if(!empty($info['basename']) && is_readable($name)) < $this->name = $info['basename']; // Определяем MIME тип файла $this->mime = mime_content_type($name); // Загружаем файл $content = file_get_contents($name); // Проверяем успешно ли был загружен файл if($content!==false) $this->content = $content; else throw new Exception('Don`t get content - "'.$name.'"'); > else throw new Exception('Error param'); > else < // сохраняем имя файла $this->name = $name; // Если не был передан тип MIME пытаемся сами его определить if(is_null($mime)) $mime = mime_content_type($name); // Сохраняем тип MIME файла $this->mime = $mime; // Сохраняем в свойстве класса содержимое файла $this->content = $content; >; > // Метод возвращает имя файла public function Name() < return $this->name; > // Метод возвращает тип MIME public function Mime() < return $this->mime; > // Метод возвращает содержимое файла public function Content() < return $this->content; > >;
Теперь собственно сам класс по формированию тела multipart/form-data для POST запроса:
class BodyPost < //Метод формирования части составного запроса public static function PartPost($name, $val) < $body = 'Content-Disposition: form-data; name="' . $name . '"'; // Проверяем передан ли класс oFile if($val instanceof oFile) < // Извлекаем имя файла $file = $val->Name(); // Извлекаем MIME тип файла $mime = $val->Mime(); // Извлекаем содержимое файла $cont = $val->Content(); $body .= '; filename="' . $file . '"' . "\r\n"; $body .= 'Content-Type: ' . $mime ."\r\n\r\n"; $body .= $cont."\r\n"; > else $body .= "\r\n\r\n".urlencode($val)."\r\n"; return $body; > // Метод формирующий тело POST запроса из переданного массива public static function Get(array $post, $delimiter='-------------0123456789') < if(is_array($post) && !empty($post)) < $bool = false; // Проверяем есть ли среди элементов массива файл foreach($post as $val) if($val instanceof oFile) ; if($bool) < $ret = ''; // Формируем из каждого элемента массива, составное тело POST запроса foreach($post as $name=>$val) $ret .= '--' . $delimiter. "\r\n". self::PartPost($name, $val); $ret .= "--" . $delimiter . "--\r\n"; > else $ret = http_build_query($post); > else throw new \Exception('Error input param!'); return $ret; > >;
Данный класс состоит из нескольких методов. Метод — PartPost формирует отдельные части составного запроса, а метод — Get объединяет эти части и формирует тело POST запроса в формате — multipart/form-data.
Теперь у нас есть универсальный класс для отправки тела POST запроса. Осталось написать программу использующую данный класс для отправки файлов на удаленный Web сервер. Воспользуемся библиотекой CURL:
// Подключаем класс-контейнер содержимого файла include "ofile.class.php"; // Подключаем класс для формирования тела POST запроса include "bodypost.class.php"; // Генерируем уникальную строку для разделения частей POST запроса $delimiter = '-------------'.uniqid(); // Формируем объект oFile содержащий файл $file = new oFile('sample.txt', 'text/plain', 'Content file'); // Формируем тело POST запроса $post = BodyPost::Get(array('field'=>'text', 'file'=>$file), $delimiter); // Инициализируем CURL $ch = curl_init(); // Указываем на какой ресурс передаем файл curl_setopt($ch, CURLOPT_URL, 'http://server/upload/'); // Указываем, что будет осуществляться POST запрос curl_setopt($ch, CURLOPT_POST, 1); // Передаем тело POST запроса curl_setopt($ch, CURLOPT_POSTFIELDS, $post); /* Указываем дополнительные данные для заголовка: Content-Type - тип содержимого, boundary - разделитель и Content-Length - длина тела сообщения */ curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: multipart/form-data; boundary=' . $delimiter, 'Content-Length: ' . strlen($post))); // Отправляем POST запрос на удаленный Web сервер curl_exec($ch);Если CURL не подходит, то данную библиотеку можно применить и для отправки через сокеты. Ну и собственно ссылки на источники:
- сайт документации php.net
- статья CURL: POST запрос, составное содержимое
- википендия: multipart/form-data
- RFC7578
Отправить файл в POST-запросе
Чтобы отправить файл в POST-запросе, используют функцию curl_file_create() , которая стала доступна с версии PHP 5.5.0.
// переменные для POST-запроса $post = [ 'var_name' => 'value', 'userfile' => curl_file_create('path/file.jpg'), ]; // отправка POST-запроса $ch = curl_init('http://example.com/'); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $post); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $data = curl_exec($ch);
Обратится к данному файлу можно через переменную $_FILES .
// загрузка файла move_uploaded_file($_FILES['userfile']['tmp_name'], 'upload/new-file.jpg');
Обновлено: 01 ноября 2020
Комментарии
Авторизуйтесь, чтобы добавлять комментарии
Как отправить файл post запросом
Никак не получается выгрузить на сайт файл, POST запросом(
Вот мой код. Хочу загрузить на сайт текстовый документ, который на компе «C:\temp.txt»
В ответ приходит ошибка, файл не загружен.
ТекстДляОтправки = «»;
Boundary = Строка(Новый УникальныйИдентификатор());
ТекстДляОтправки = ТекстДляОтправки + «—«+Boundary + Символы.ПС;
ТекстДляОтправки = ТекстДляОтправки + «Content-Disposition: form-data; name=»»file»»; filename=»»file»»» + Символы.ПС;
ТекстДляОтправки = ТекстДляОтправки + «Content-Type: application/x-octet-stream» + Символы.ПС;
ДД = Новый ДвоичныеДанные(«C:\temp.txt»);
Данные = Base64Строка(ДД);
ТекстДляОтправки = ТекстДляОтправки + Символы.ПС;
ТекстДляОтправки = ТекстДляОтправки + Данные + Символы.ПС;
ТекстДляОтправки = ТекстДляОтправки + «—«+Boundary + Символы.ПС;
Заголовки = Новый Соответствие();
Заголовки.Вставить(«Content-Type», «multipart/form-data; boundary=» + Boundary);
Заголовки.Вставить(«Content-Lenght», XMLСтрока(СтрДлина(ТекстДляОтправки)));
Заголовки.Вставить(«authorization», «6q3gCH0mFJWN2qXswQ8WYexfoYPV0KFf»);
HHTPЗапрос = Новый HTTPЗапрос(Подключение.Адрес, Заголовки);
HHTPЗапрос.УстановитьТелоИзСтроки(ТекстДляОтправки);
HTTPОтвет = Подключение.HTTP.ОтправитьДляОбработки(HHTPЗапрос);
ТекстОтвета = HTTPОтвет.ПолучитьТелоКакСтроку();
Сообщить(ТекстОтвета);
Создаю запрос через программу postman, через нее файл грузится. Т.е. проблема в 1С
Что я делаю не так, подскажите плиз.
В файле кириллица есть? Может кодировка? Если отправить тестовый файл с одной строкой латинницей, то отправится?
Фидлер и сравнивай что через постман а что через 1С уходит
(1) В файле число
(2) А как в фидлере посмотреть, что я отправил из 1с?
Может там в файле BOM добавляется?
Имхо «RFC 7231» это на текущий момент редкостный изврат и пора бы уже на нечто более вменяемое переходить.
На тот же JSON (чистый application/json) вместо кривого текстового протокола с кучей особенностей.
(0) Content-Length — очепятка.
(7) Как через JSON отправить файл?
(7) Это я для теста взял блокнот для отправки. Мне нужно выгружать на сайт pdf, картинки и файлы word
HTTPСоединение = Новый HTTPСоединение(«1cvpn.ru»,1717);
HTTPЗапрос = Новый HTTPЗапрос(АдресРесурса);
HTTPЗапрос.Заголовки.Вставить(«Content-type», «application/json»);
HTTPЗапрос.УстановитьТелоИзСтроки(«»+СтрокаJSON, КодировкаТекста.UTF8, ИспользованиеByteOrderMark.НеИспользовать);
Ответ = HTTPСоединение.ОтправитьДляОбработки(HTTPЗапрос);
Сообщить(«Код результата: » + Ответ.КодСостояния);
Сообщить(«Ответ: » + Ответ.ПолучитьТелоКакСтроку(«UTF-8»));
(10) Ищи пример как для твоего сайта можно/нужно выгружать правильно.
HTTP протокол он весьма сложный и замороченный, куча глюков бывает и несоответствий протоколу в конкретных реализациях.
(12) Из постмана я могу увидеть, как выглядит запрос, который отправляю. Но не на языке 1С.
Мне кажется, что сервер не может расшифровать файл из формата base64.
А как можно отправить из 1С без кодирования в base64?
Вот пример кода curl
curl -X POST \
https:Сервер \
-H ‘authorization: zLaCON1OVO. (оставшаяся часть ключа)’ \
-H ‘content-type: multipart/form-data; boundary=—-WebKitFormBoundary7MA4YWxkTrZu0gW’ \
-F file=@/home/file.doc
(14) И как это выглядит в фидлере?
‘https://Сервер’ \
-H ‘Accept: */*’ \
-H ‘Connection: keep-alive’ \
-H ‘Content-Type: application/x-www-form-urlencoded’ \
-H ‘Host: Сервер’ \
-H ‘Postman-Token: 2689481c-511c-4d0f-bd2a-46947e9ebaa8,e6b697f6-884b-4738-99f3-23938ea2e6b1’ \
-H ‘User-Agent: PostmanRuntime/7.15.0’ \
-H ‘accept-encoding: gzip, deflate’ \
-H ‘authorization: 6q3gCH0mFJWN2qXswQ8WYexfoYPV0KFf’ \
-H ‘cache-control: no-cache,no-cache’ \
-H ‘content-length: 209’ \
-H ‘content-type: multipart/form-data; boundary=—-WebKitFormBoundary7MA4YWxkTrZu0gW’ \
-H ‘cookie: sails.sid=s%3AkmiGqtMpB3pRu5N4gUGcLzHA-5ZG9gwP.JtLaAZ%2B3J98xTNAXX5JJR2I0coHYAKL5qJoWXwM%2FFWM’ \
-b sails.sid=s%3AkmiGqtMpB3pRu5N4gUGcLzHA-5ZG9gwP.JtLaAZ%2B3J98xTNAXX5JJR2I0coHYAKL5qJoWXwM%2FFWM \
-F file=@/C:/temp.txt
(16) Да нафик этот постман, ты фидлер покажи!
(13) УстановитьТелоИзДвоичныхДанных()
(18) Пробовал, не работает.
(17) ща поставлю фидлер, я им не пользуюсь
(19) Кстати в 1С «УстановитьТело» по сути же присоединяет к телу запросу данные а не замещает их?
Или есть какие то варианты между разными «УстановитьТелоИзСтроки», «УстановитьТелоИзДвоичныхДанных» и «УстановитьИмяФайлаТела» ?
(21)+ В смысле что будет если дважды вызвать «УстановитьТелоИзДвоичныхДанных» например?
В фидлере при отправке из 1С:
—55170dc4-4c22-4ad8-8d72-9870449e5bbb
Content-Disposition: form-data; name=»file»; filename=»file»
Content-Type: application/x-octet-stream
(23) Он заменяет тело запроса последней строкой
(24) А из curl или из postman что в fiddler ?
УстановитьТелоИзДвоичныхДанных при отправке запроса просто скопировала в тело запроса текст из блокнота. Т.е. она не добавляет двоичные данные в запрос, а пытается составить запрос из переданных двоичных данных
У тебя же задача довольно простая, имея рабочий пример и смотря по сути «текстовый результат» в фидлере, добиться чтобы 1С выдавала такой же текст.
HTTP это текстовый протокол, где все в банальный текст превращается, который и уходит запакованный обычно в gzip
(27) Это 1С «—55170dc4-4c22-4ad8-8d72-9870449e5bbb» взяла из заголовка Boundary или сама придумала свой?
(28) Я понимаю)
Postman перестал работать с включенным фиддлером)
(29) да, это boundary
А сервер при отправке из 1С что отвечает?
(30) В постмане тоже как прокси указал фидлер?
Хотя постман вроде по «Code» текст запроса показывает, фидлер не обязателен
(33)
POST Сервер HTTP/1.1
Host: installRequests.api.x-keeper.net
Content-Type: multipart/form-data; boundary=—-WebKitFormBoundary7MA4YWxkTrZu0gW
authorization: 6q3gCH0mFJWN2qXswQ8WYexfoYPV0KFf
cache-control: no-cache,no-cache
Postman-Token: fe541c6f-d25d-47fb-be57-c5c80c5f885d
Content-Disposition: form-data; name=»file»; filename=»/C:/temp.txt
(34) В теле из 1С Boundary получился —55170dc4-4c22-4ad8-8d72-9870449e5bbb, а в заголовке какой? Судя по коду — без — в начале.
(34) Чтобы на 1С сделать так же надо «УстановитьИмяФайлаТела»
(36) Вместо блокнота подставил файл ворд. В фиддлере нечитаемые символы ࡱ
(35) Фиддлер не показывает, что было в заголовке запроса.
В теле запроса вначале Boundary и в конце
(35) А в коде я вставляю Boundary:
Заголовки.Вставить(«Content-Type», «multipart/form-data; boundary=» + Boundary);
(39) Это в заголовки. А в тело:
ТекстДляОтправки = ТекстДляОтправки + «—«+Boundary + Символы.ПС;
(40)
В теле 2 раза
ТекстДляОтправки = ТекстДляОтправки + «—«+Boundary + Символы.ПС;
ТекстДляОтправки = ТекстДляОтправки + «Content-Disposition: form-data; name=»»file»»; filename=»»file»»» + Символы.ПС;
ТекстДляОтправки = ТекстДляОтправки + «Content-Type: application/x-octet-stream» + Символы.ПС;
ДД = Новый ДвоичныеДанные(«C:\Word.doc»);
Данные = Base64Строка(ДД);
ТекстДляОтправки = ТекстДляОтправки + Символы.ПС;
ТекстДляОтправки = ТекстДляОтправки + Данные + Символы.ПС;
ТекстДляОтправки = ТекстДляОтправки + «—«+Boundary + Символы.ПС;
(41) Так какой ответ приходит от сервера в итоге?
Кстати, последний разделитель должен оканчиваться «—» + Boundary + «—» + Символы.ПС
(43) Подправил, но ошибка та же:
Файл не загружен. Проверьте ваш запрос, обратитесь к документации.
Ураа у меня получилось!!
Спасибо всем, кто не прошел мимо)
Вот код, может быть пригодится:
ИмяФайлаОтправки = КаталогВременныхФайлов() + «post.txt»;
ИмяФайлаДляЗагрузки = «C:\Word.doc»;
Сервер = «installRequests.api.x-keeper.net:443»;
Подключение = Новый Структура («HTTP, ЗаголовокHTTP, Адрес» );
Подключение.Адрес = «v1/requests.uploadFile?requestNb=НомерЗаявки&fileCatPreffix=ИДФайла»;
Подключение.Адрес = СтрЗаменить(Подключение.Адрес,»НомерЗаявки», «784-N71»);
Подключение.Адрес = СтрЗаменить(Подключение.Адрес,»ИДФайла», «scan_dover_prikaz_postavshik»);
HTTP = Новый HTTPСоединение(Сервер. Новый ЗащищенноеСоединениеOpenSSL);
Boundary = Строка(Новый УникальныйИдентификатор());
//Определяем массив для процедуры ОбъединитьФайлы
МассивФайловДляОбъединения = Новый Массив;
//Формируем начальный фрагмент файла POST-запроса
ИмяФайлаОтправкиНачало = ПолучитьИмяВременногоФайла(«txt»);
ФайлОтправкиНачало = Новый ЗаписьТекста(ИмяФайлаОтправкиНачало, КодировкаТекста.ANSI, Символы.ПС, Ложь);
//Определяем раздел двоичных данных
ФайлОтправкиНачало.ЗаписатьСтроку(«—» + Boundary);
//Указываем имя файла для передачи
ФайлОтправкиНачало.ЗаписатьСтроку(«Content-Disposition: form-data; name=»»file»»; filename=»»» + ИмяФайлаДляЗагрузки + «»»»);
//Указываем тип передаваемых данных.
ФайлОтправкиНачало.ЗаписатьСтроку(«Content-Type: application/x-octet-stream»);
ФайлОтправкиНачало.ЗаписатьСтроку(«»);
ФайлОтправкиНачало.Закрыть();
//Копируем файл для загрузки во временный и добавляем в массив файлов для объединения
ИмяФайлаДляЗагрузкиВременный = ПолучитьИмяВременногоФайла(«tmp»);
КопироватьФайл(ИмяФайлаДляЗагрузки, ИмяФайлаДляЗагрузкиВременный);
МассивФайловДляОбъединения.Добавить(ИмяФайлаДляЗагрузкиВременный);
//Формируем конечный фрагмент файла POST-запроса
ИмяФайлаОтправкиКонец = ПолучитьИмяВременногоФайла(«txt»);
ФайлОтправкиКонец = Новый ЗаписьТекста(ИмяФайлаОтправкиКонец, КодировкаТекста.ANSI, Символы.ПС, Ложь);
//Завершение раздела двоичных данных
ФайлОтправкиКонец.ЗаписатьСтроку(«—» + Boundary);
ФайлОтправкиКонец.ЗаписатьСтроку(«—» + Boundary + «—«);
ФайлОтправкиКонец.Закрыть();
//Теперь сформированные фрагменты сообщения для сервера объединяем в один файл POST-запроса
ОбъединитьФайлы(МассивФайловДляОбъединения, ИмяФайлаОтправки);
Заголовки = Новый Соответствие();
Заголовки.Вставить(«Content-Type», «multipart/form-data; boundary=» + Boundary);
HHTPЗапрос = Новый HTTPЗапрос(Подключение.Адрес, Заголовки);
HHTPЗапрос.УстановитьИмяФайлаТела(ИмяФайлаОтправки);
HTTPОтвет = HTTP.ОтправитьДляОбработки(HHTPЗапрос);
ТекстОтвета = HTTPОтвет.ПолучитьТелоКакСтроку();
Сообщить(ТекстОтвета);