Почему данные в теле HTTP GET-запроса не отправляются в 1С и как правильно передавать фильтры для API?

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

При работе с внешними облачными сервисами и их API мы часто сталкиваемся с необходимостью отправки запросов для получения данных. Иногда возникают ситуации, когда, следуя документации API, мы используем метод GET, но обнаруживаем, что фильтры, переданные в теле запроса, игнорируются, и сервер возвращает полный набор данных. В этом материале мы подробно разберем, почему такая ситуация происходит при использовании платформы 1С, как она соответствует или не соответствует общепринятым HTTP-стандартам, и какое решение позволяет корректно передавать параметры фильтрации.

Представим, что у нас есть задача получить данные из облачного сервиса (например, акты списания продуктов) за определенный период. Документация API сервиса указывает на необходимость использования метода GET, при этом фильтры по датам должны передаваться в теле запроса в формате JSON. Мы реализовали код на 1С, который формирует JSON-строку с фильтрами и устанавливает её в тело HTTPЗапрос с помощью метода .УстановитьТелоИзСтроки(). Однако в ответ от сервера мы получаем не отфильтрованный список документов за нужный месяц, а все документы, когда-либо созданные в системе.

Давайте проанализируем исходную проблему и шаги, предпринятые для её решения.

Исходная реализация запроса в 1С

Изначально код для формирования запроса выглядел следующим образом:


// Функция для получения основных заголовков запроса
&НаСервере
Функция ф_ПолучитьОсновныеЗаголовкиЗапросаGET()
    
    ЗаголовкиЗапроса = Новый Соответствие;
    ЗаголовкиЗапроса.Вставить("Authorization", "Basic ХХХХХХХХХХХХХХХХХХХХХХХХХХ==");
    ЗаголовкиЗапроса.Вставить("Connection", "keep-alive");
    ЗаголовкиЗапроса.Вставить("Content-Type", "application/json");
    ЗаголовкиЗапроса.Вставить("Host", "hungrywolf.quickresto.ru");
    
    Возврат ЗаголовкиЗапроса;
    
КонецФункции

// Функция для формирования параметров фильтрации в JSON
&НаСервере
Функция ф_ПолучитьПараметрыВТелеЗапросаJSON()
    ПериодыСоотв = Новый Соответствие;
    ПериодыСоотв.Вставить("since", Строка(Формат(НачалоДня(ЭтаФорма.ДатаНачала), "ДФ=гггг-ММ-дд" )) + "T00:00:00.000Z");
    ПериодыСоотв.Вставить("till", Строка(Формат(КонецДня(ЭтаФорма.ДатаОкончания), "ДФ=гггг-ММ-дд")) + "T23:59:59.999Z");
    
    ОтборыСоотв = Новый Соответствие;
    ОтборыСоотв.Вставить("field", "invoiceDate");
    ОтборыСоотв.Вставить("operation", "range");
    ОтборыСоотв.Вставить("value", ПериодыСоотв);
    
    МассивОтборов = Новый Массив;
    МассивОтборов.Добавить(ОтборыСоотв);
    
    ФильтрыСоотв = Новый Соответствие;
    ФильтрыСоотв.Вставить("filters", МассивОтборов);
    
    Фильтры = Новый ФиксированноеСоответствие(ФильтрыСоотв);
    Данные = Новый ЗаписьJSON();
    Данные.УстановитьСтроку();
    ЗаписатьJSON(Данные, Фильтры);
    
    Итог = Данные.Закрыть();
        
    Возврат Итог;
КонецФункции

// Процедура соединения и отправки запроса
&НаКлиенте
Процедура СодеинитьсяССерверомКлиент()
    
    Соедин1 = Новый HTTPСоединение("hungrywolf.quickresto.ru",443,,,,60, Новый ЗащищенноеСоединениеOpenSSL());
    ПолученныйДжсон = ф_ПолучитьПараметрыВТелеЗапросаJSON();
    
    ШаблонЗапроса = СтрШаблон("/platform/online/api/list?moduleName=%1", ЭтаФорма.КакойДокумент);
    
    ЗаголовкиЗапроса = ф_ПолучитьОсновныеЗаголовкиЗапросаGET();
    Запрос1 = Новый HTTPЗапрос(ШаблонЗапроса, ЗаголовкиЗапроса);
    Запрос1.УстановитьТелоИзСтроки(ПолученныйДжсон, КодировкаТекста.UTF8); // Здесь устанавливается тело запроса
    
    Попытка
        Ответ = Соедин1.ВызватьHTTPМетод("GET", Запрос1); // Вызов метода GET
        
        Если Ответ.КодСостояния = 200 Тогда
            ЧтенДЖСОН = Новый ЧтениеJSON;
            ЧтенДЖСОН.УстановитьСтроку(Ответ.ПолучитьТелоКакСтроку());
            ОтветСоотв = ПрочитатьJSON(ЧтенДЖСОН, Истина);
            ЧтенДЖСОН.Закрыть();
        Иначе
            Сообщить("Код состояния ответа: " + Ответ.КодСостояния + Символы.ПС + "Причина: " + Ответ.ПолучитьТелоКакСтроку());
        КонецЕсли;
    Исключение
        Сообщить(ОписаниеОшибки());
    КонецПопытки;
    
КонецПроцедуры

Несмотря на то, что код формировал JSON с фильтрами и присваивал его телу запроса, сервер возвращал полный объем данных. Это навело на мысль, что тело запроса методом GET не отправляется вовсе. Возникает вопрос: почему так происходит?

Особенности метода GET согласно HTTP-стандартам

Чтобы понять причину такого поведения, давайте обратимся к стандартам протокола HTTP. Метод GET разработан специально для запроса данных с сервера. Его основное предназначение — это получение ресурса без внесения изменений на сервере. Это делает его «безопасным» (safe) и «идемпотентным» (idempotent), что означает, что многократное выполнение запроса не приводит к изменению состояния сервера.

Традиционно, параметры для GET-запросов передаются через строку запроса (query string) в URL, например, /users?id=123&active=true. Это позволяет легко кэшировать такие запросы и делает их видимыми в логах сервера.

Исторически в спецификации HTTP/1.1 (RFC 2616) прямо указывалось, что тело запроса в GET-методе должно игнорироваться. Это было четкое указание для всех реализаций HTTP-клиентов и серверов.

Однако, в более новых спецификациях HTTP/1.1 (RFC 7231, выпущенной в 2014 году, которая заменила RFC 2616) было сделано уточнение, что "полезная нагрузка в сообщении GET-запроса не имеет определенной семантики". Это означает, что протокол напрямую не запрещает наличие тела у GET-запроса, но его семантика по умолчанию не определена. Другими словами, современные стандарты HTTP не говорят, что тело GET-запроса не может существовать, но и не определяют, как его следует интерпретировать. Это оставляет лазейку для нестандартных реализаций, которые могут принимать тело в GET-запросах, но в большинстве случаев это все еще считается плохой практикой.

Почему отправка тела в GET-запросе – плохая практика?

  1. Нарушение стандартов и принципов REST: Это противоречит общепринятым принципам RESTful API, где GET предназначен исключительно для получения данных, а POST — для отправки данных в теле.
  2. Несовместимость: Многие существующие реализации серверов, прокси-серверов, балансировщиков нагрузки и клиентских библиотек (в том числе и в 1С) могут игнорировать или отбрасывать тело GET-запроса. Это приводит к непредсказуемому поведению или потере данных, как в нашем случае. Некоторые фреймворки могут даже отклонять такие запросы, вызывая ошибки.
  3. Проблемы с кэшированием: GET-запросы по своей природе кэшируемы. Если в них присутствует тело, механизмы кэширования могут не распознать его как часть запроса, что приведет к некорректному кэшированию или полному отсутствию кэширования, что негативно скажется на производительности.
  4. Безопасность: Параметры GET-запросов часто логируются в URL, что делает их менее безопасными для передачи конфиденциальной информации по сравнению с POST-запросами, где данные передаются в теле запроса и не отображаются в логах URL.
  5. Ограничение по размеру: Передача данных в URL имеет ограничения по длине (зависит от сервера и браузера), в то время как тело запроса POST позволяет передавать значительно большие объемы данных и сложные структуры, такие как JSON или XML.

Несмотря на эти рекомендации, некоторые облачные сервисы или специфические реализации (например, Elasticsearch) могут принимать тело в GET-запросах для реализации сложных запросов, фильтрации или агрегации данных, где URL может стать слишком длинным и неудобным. В таких случаях тело запроса используется для Domain Specific Language (DSL), который позволяет структурировать сложные условия. Однако, это является скорее исключением, чем общепринятой практикой.

Поведение платформы 1С при использовании GET-запроса с телом

Документация платформы 1С четко регулирует поведение метода HTTPСоединение.ВызватьHTTPМетод(). В описании этого метода есть критически важное примечание:


Примечание:
При использоватения метода GET тело запроса на сервер отправлено не будет.

Это однозначно подтверждает, что платформа 1С следует традиционным и наиболее распространенным стандартам HTTP, игнорируя попытки установить тело для GET-запроса. Таким образом, даже если мы вызываем Запрос1.УстановитьТелоИзСтроки(ПолученныйДжсон, КодировкаТекста.UTF8);, эти данные не будут отправлены на сервер при использовании "GET" в методе ВызватьHTTPМетод().

Именно это и приводило к проблеме: наш JSON-фильтр не достигал облачного сервера, и он, не имея фильтров, возвращал все доступные данные.

Решение проблемы: Использование метода POST

Самым логичным и стандартным решением в данной ситуации является использование метода POST вместо GET. Метод POST специально предназначен для отправки данных в теле запроса на сервер. Несмотря на то, что документация API облачного сервиса могла рекомендовать GET, на практике при необходимости передачи сложных фильтров в теле запроса POST является более подходящим выбором.

Давайте посмотрим, как изменится код:


// ... (Функции ф_ПолучитьОсновныеЗаголовкиЗапросаGET() и ф_ПолучитьПараметрыВТелеЗапросаJSON() остаются без изменений) ...

&НаКлиенте
Процедура СодеинитьсяССерверомКлиент()
    
    Соедин1 = Новый HTTPСоединение("hungrywolf.quickresto.ru",443,,,,60, Новый ЗащищенноеСоединениеOpenSSL());
    ПолученныйДжсон = ф_ПолучитьПараметрыВТелеЗапросаJSON();
    
    ШаблонЗапроса = СтрШаблон("/platform/online/api/list?moduleName=%1", ЭтаФорма.КакойДокумент);
    
    ЗаголовкиЗапроса = ф_ПолучитьОсновныеЗаголовкиЗапросаGET();
    Запрос1 = Новый HTTPЗапрос(ШаблонЗапроса, ЗаголовкиЗапроса);
    Запрос1.УстановитьТелоИзСтроки(ПолученныйДжсон, КодировкаТекста.UTF8);
    
    Попытка
        // Изменяем "GET" на "POST"
        Ответ = Соедин1.ВызватьHTTPМетод("POST", Запрос1); 
        
        Если Ответ.КодСостояния = 200 Тогда
            ЧтенДЖСОН = Новый ЧтениеJSON;
            ЧтенДЖСОН.УстановитьСтроку(Ответ.ПолучитьТелоКакСтроку());
            ОтветСоотв = ПрочитатьJSON(ЧтенДЖСОН, Истина);
            ЧтенДЖСОН.Закрыть();
            // Теперь ОтветСоотв должен содержать отфильтрованные данные
        Иначе
            Сообщить("Код состояния ответа: " + Ответ.КодСостояния + Символы.ПС + "Причина: " + Ответ.ПолучитьТелоКакСтроку());
        КонецЕсли;
    Исключение
        Сообщить(ОписаниеОшибки());
    КонецПопытки;
    
КонецПроцедуры

После изменения метода на "POST", облачный сервер начал корректно обрабатывать переданный JSON-фильтр и возвращать только необходимые документы за указанный месяц. Это подтверждает, что проблема заключалась именно в невозможности отправки тела запроса методом GET из 1С.

Альтернативные подходы к передаче фильтров

Если бы API не требовал передачи JSON в теле запроса, а допускал бы использование традиционных параметров запроса (query parameters) для GET-метода, мы могли бы рассмотреть следующие варианты:

  1. Простые параметры запроса (Query Parameters): Для небольшого количества простых фильтров. Например, если бы API принимал /platform/online/api/list?moduleName=act&since=2023-01-01T00:00:00.000Z&till=2023-01-31T23:59:59.999Z. В этом случае мы бы формировали URL-строку с добавлением этих параметров.
  2. Пример формирования URL с параметрами:

    
    // Предположим, что API ожидает параметры в URL
    Функция ф_ПолучитьСтрокуПараметровЗапроса()
        ПериодыСоотв = Новый Соответствие;
        ПериодыСоотв.Вставить("since", Строка(Формат(НачалоДня(ЭтаФорма.ДатаНачала), "ДФ=гггг-ММ-дд" )) + "T00:00:00.000Z");
        ПериодыСоотв.Вставить("till", Строка(Формат(КонецДня(ЭтаФорма.ДатаОкончания), "ДФ=гггг-ММ-дд")) + "T23:59:59.999Z");
            
        СтрокаПараметров = "";
        Для Каждого КлючЗначение Из ПериодыСоотв Цикл
            Если Не ПустаяСтрока(СтрокаПараметров) Тогда
                СтрокаПараметров = СтрокаПараметров + "&";
            КонецЕсли;
            СтрокаПараметров = СтрокаПараметров + КлючЗначение.Ключ + "=" + ИнтернетКодировать(КлючЗначение.Значение);
        КонецЦикла;
        Возврат СтрокаПараметров;
    КонецФункции
    
    // ... в процедуре СодеинитьсяССерверомКлиент() ...
    ШаблонЗапроса = СтрШаблон("/platform/online/api/list?moduleName=%1&%2", ЭтаФорма.КакойДокумент, ф_ПолучитьСтрокуПараметровЗапроса());
    Запрос1 = Новый HTTPЗапрос(ШаблонЗапроса, ЗаголовкиЗапроса);
    // Здесь .УстановитьТелоИзСтроки() не вызывается, так как все в URL
    

    Однако, данный подход не был применим в нашем случае, так как API требовал сложную JSON-структуру фильтров именно в теле запроса.

Итоги и рекомендации

Мы выяснили, что основная причина проблемы заключалась в несоответствии ожиданий от API и стандартного поведения метода GET в платформе 1С. Важно помнить, что:

  1. Метод GET в 1С (и в большинстве других реализаций) не предназначен для отправки тела запроса. Платформа 1С явно игнорирует данные, установленные через .УстановитьТелоИзСтроки() для GET-запросов.
  2. Для передачи данных в теле запроса всегда следует использовать метод POST (или PUT/PATCH, в зависимости от семантики операции).
  3. При работе с внешними API, если документация указывает на GET, но при этом требует передачи сложной структуры данных в теле, почти всегда это является индикатором того, что метод POST будет более корректным и рабочим решением, даже если это расходится с первоначальной документацией. Рекомендуется всегда пробовать POST в таких случаях.
  4. Если API поддерживает передачу фильтров через query string, это предпочтительный способ для GET-запросов, но для сложных структур он может быть непрактичен.

При интеграции с внешними системами всегда анализируйте не только предоставленную документацию, но и общепринятые HTTP-стандарты, а также особенности реализации HTTP-клиентов в используемой платформе (в нашем случае, 1С). Такой подход позволит избежать подобных проблем и создавать надежные и эффективные интеграции.

← На главную