HTTP получил метод QUERY: зачем понадобился безопасный запрос с телом

от автора

В июне 2026 года опубликован RFC 10008 — The HTTP QUERY Method. Он добавил в HTTP новый стандартизованный метод QUERY, который также появился в реестре HTTP‑методов IANA: запрос с телом к конкретному адресу, который по смыслу остаётся чтением и не должен менять данные на сервере.

Метод нужен для знакомого случая: нужно передать сложные параметры поиска, фильтрации или отчёта, а по семантике это всё ещё безопасная операция чтения.

Например:

  • вывести все мои заказы с 1 июля 2025 года по сегодня, только оплаченные, но ещё не полученные;

  • дать из системы статистики срез посещений только для посетителей из Казахстана, только в дневное время и только для браузеров на macOS;

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

Обычный GET для таких запросов часто неудобен: параметры быстро превращаются в длинный URI или в собственный мини‑язык фильтрации внутри строки запроса. POST удобнее для передачи структуры, но передаёт не тот сигнал промежуточной инфраструктуре.

Напомню: HTTP как протокол появился раньше своих RFC: в RFC 1945 сказано, что HTTP используется в Вебе с 1990 года. Сам RFC 1945 вышел в мае 1996 года и описал HTTP/1.0 — фактически зафиксировал распространённую к тому моменту практику. После этого HTTP много раз уточнялся: HTTP/1.1, разделение семантики и транспорта, HTTP/2, HTTP/3. Появление нового метода в 2026 году — ещё одно уточнение в протоколе, который всё ещё развивается.

Исходная проблема

Когда нужно просто получить страницу, объект или список по понятному адресу, пусть даже с несколькими уточняющими параметрами, GET подходит хорошо:

GET /orders?status=paid&limit=100 HTTP/1.1Host: api.example.com

Такой запрос легко воспроизвести, положить в закладку, записать в журнал и сохранить в кэше. Браузеры, прокси, CDN, серверные фреймворки и средства диагностики десятилетиями хорошо понимают эту модель.

Но в API часто встречается другой сценарий: поиск по вложенным условиям, выбор набора полей, сортировка, диапазоны дат, группировки, ограничения по правам доступа. Например:

{  "where": {    "and": [      { "field": "status", "eq": "paid" },      { "field": "created_at", "gte": "2026-01-01" },      { "field": "email", "like": "%@example.com" }    ]  },  "select": ["id", "email", "total", "created_at"],  "sort": ["-created_at"],  "limit": 100}

Это можно закодировать в строку запроса URI, но дальше начинаются обычные неудобства:

  • URI становится длинным и плохо читается;

  • появляются ограничения на длину URI в клиентах, прокси, балансировщиках и серверах;

  • сложные структуры приходится кодировать в формат, который для них не очень удобен;

  • параметры чаще попадают в журналы доступа, историю браузера, отчёты WAF, CDN и APM;

  • каждая комбинация параметров выглядит как отдельный URI, даже если для приложения это один и тот же вид операции.

RFC 10008 прямо называет такие причины среди мотивов для QUERY. В частности, документ напоминает, что разные участники цепочки обработки могут иметь собственные лимиты, и заранее знать самый строгий из них обычно нельзя.

Почему не GET с телом

На первый взгляд можно было бы оставить метод GET, но передавать сложные параметры в теле:

GET /orders/search HTTP/1.1Host: api.example.comContent-Type: application/json{  "status": "paid",  "limit": 100}

Технически HTTP‑сообщение может содержать тело и при GET. Проблема в другом: семантика GET не задаёт смысл такого тела. В RFC 9110 сказано, что содержимое в GET не имеет общей определённой семантики, не может менять смысл запроса и целевой URI, а некоторые реализации могут отклонять такие запросы по соображениям совместимости или безопасности.

На практике запрос проходит не только через приложение. Между клиентом и обработчиком могут быть:

  • клиентская библиотека;

  • браузер;

  • CDN;

  • WAF;

  • балансировщик нагрузки;

  • обратный прокси;

  • API‑шлюз;

  • ingress‑контроллер;

  • промежуточный обработчик авторизации;

  • кэш;

  • трассировка и сбор метрик.

Если хоть один компонент посчитает GET с телом «необычным случаем», поведение становится зависимым от конкретной инфраструктуры. Где‑то тело будет проигнорировано, где‑то запрос заблокируют, где‑то фреймворк не даст прочитать тело в обычном обработчике. Внутри одной системы это ещё можно удержать под контролем, но для публичного и переносимого описания API такой вариант создаёт слишком много оговорок.

Почему не POST /search

Самый распространённый обходной путь — использовать POST:

POST /orders/search HTTP/1.1Host: api.example.comContent-Type: application/jsonAccept: application/json{  "status": "paid",  "limit": 100}

Этот вариант рабочий. Его понимают фреймворки, прокси и клиенты. Многие API построены именно на таком подходе и будут так жить ещё долго.

Минус в том, что POST сам по себе не говорит инфраструктуре, что операция безопасная и идемпотентная. Скажем, разработчики конкретного сервиса знают, что POST /orders/search только ищет заказы и не меняет данные на сервере, так что такой адрес их не смущает. Однако всё, что идёт дальше по цепочке, про это локальное соглашение не знает. Обобщённый HTTP‑клиент, кэш, прокси, механизм повторов или инструмент документации видит просто POST. А POST может создать ресурс, изменить состояние, запустить задачу или выполнить любую другую обработку по правилам ресурса.

Из‑за этого приходится переносить важную часть смысла в имя адреса:

POST /orders/search

Слово search помогает человеку, но не меняет семантику HTTP‑метода. QUERY позволяет выразить это на уровне протокола:

QUERY /orders

Метод сообщает, что клиент просит ресурс выполнить запрос, описанный в теле, и вернуть результат без изменения состояния целевого ресурса.

Один и тот же поиск: раньше и с QUERY

Возьмём обычный пример: в интерфейсе заказов пользователь собирает фильтр. Нужны оплаченные заказы за период, только по нескольким странам, с выбором полей и сортировкой.

Через GET это часто превращается в длинную строку запроса:

GET /orders?status=paid&from=2026-01-01&to=2026-06-30&country=DE&country=FR&fields=id,total,currency,created_at&sort=-created_at&limit=100 HTTP/1.1Host: api.example.com

Пока параметров мало, такой вариант нормален. Когда появляются группы условий, вложенные and/or, полнотекстовый поиск, списки идентификаторов и настройки агрегации, URI становится плохо читаемым. Каждый, кто хоть раз смотрел на адреса сайтов на ядре Битрикс, в этом месте обычно уже не хмыкает, а тяжело вздыхает. Разработчики начинают придумывать собственные соглашения: JSON в параметре filter, base64 в строке запроса, массивы через повторяющиеся ключи, мини‑язык фильтрации внутри одного параметра.

Например:

GET /orders?filter=%7B%22status%22%3A%22paid%22%2C%22limit%22%3A100%7D HTTP/1.1Host: api.example.com

Работать это может, но диагностика становится неприятной: надо декодировать строку, следить за лимитами URI и помнить, что весь фильтр с высокой вероятностью окажется в журналах доступа.

Через POST тот же поиск выглядит удобнее для приложения:

POST /orders/search HTTP/1.1Host: api.example.comContent-Type: application/jsonAccept: application/json{  "status": "paid",  "period": {    "from": "2026-01-01",    "to": "2026-06-30"  },  "countries": ["DE", "FR"],  "fields": ["id", "total", "currency", "created_at"],  "sort": ["-created_at"],  "limit": 100}

Так проще описывать структуру запроса, валидировать её и расширять без усложнения URI. Поэтому POST /search стал привычным решением.

Но на уровне HTTP это всё равно POST. Если где‑то включён автоматический повтор запросов, кэширование, аудит опасных операций или отдельная политика для изменяющих методов, инфраструктура не знает, что перед ней только чтение. Это знание живёт в документации, названии адреса и коде сервиса.

С QUERY тот же пример можно записать так:

QUERY /orders HTTP/1.1Host: api.example.comContent-Type: application/jsonAccept: application/json{  "status": "paid",  "period": {    "from": "2026-01-01",    "to": "2026-06-30"  },  "countries": ["DE", "FR"],  "fields": ["id", "total", "currency", "created_at"],  "sort": ["-created_at"],  "limit": 100}

Разница не в количестве строк. Разница в том, что структура запроса остаётся в теле, а HTTP‑метод одновременно сообщает: это безопасная и идемпотентная операция чтения в области ресурса /orders.

В старой модели смысл приходилось распределять по нескольким местам:

  • GET говорил «чтение», но заставлял упаковывать сложный ввод в URI;

  • POST давал удобное тело, но не говорил, что операция безопасная;

  • имя /search объясняло намерение человеку, но не меняло поведение HTTP‑инфраструктуры.

QUERY собирает эти признаки в одном месте: метод остаётся методом чтения, а параметры передаются как нормальное содержимое запроса.

Что именно стандартизовал QUERY

Пример запроса:

QUERY /orders HTTP/1.1Host: api.example.comContent-Type: application/jsonAccept: application/json{  "status": "paid",  "created_after": "2026-01-01",  "fields": ["id", "email", "total", "created_at"],  "sort": ["-created_at"],  "limit": 100}

В RFC 10008 у QUERY зафиксированы несколько свойств.

Во‑первых, это безопасный метод в смысле HTTP: клиент не запрашивает и не ожидает изменения состояния целевого ресурса. Это не запрещает серверу писать журналы, считать метрики или создавать временные вспомогательные ресурсы для результата. Такие побочные действия вообще возможны и у GET.

Во‑вторых, метод идемпотентный: повтор того же запроса должен иметь тот же ожидаемый эффект, что и один запрос. Это важно для автоматических повторов после сетевого сбоя.

В‑третьих, содержимое запроса является нормальной частью семантики метода. Формат тела задаётся Content-Type, а желаемый формат ответа — обычным заголовком Accept.

Если упростить:

  • GET — безопасное чтение, параметры обычно находятся в URI;

  • POST — обработка тела запроса по правилам ресурса, возможно с изменением состояния;

  • QUERY — безопасное и идемпотентное чтение, где параметры находятся в теле запроса.

Формат тела и Accept‑Query

Так как смысл QUERY находится в теле запроса, сервер должен понимать его формат. RFC 10008 требует отклонять запрос, если Content-Type отсутствует или не соответствует содержимому.

Например, ресурс может принимать JSON:

QUERY /contacts HTTP/1.1Host: api.example.comContent-Type: application/jsonAccept: application/json{  "select": ["surname", "given_name", "email"],  "limit": 10,  "match": {    "email": "*@example.com"  }}

Или application/x-www-form-urlencoded:

QUERY /contacts HTTP/1.1Host: api.example.comContent-Type: application/x-www-form-urlencodedAccept: application/jsonselect=surname,given_name,email&limit=10&match=email%3D*%40example.com

Или специализированный формат, например JSONPath:

QUERY /errata.json HTTP/1.1Host: api.example.comContent-Type: application/jsonpathAccept: application/json$..[?@.status=="Rejected" && @.submit_date>"2024"]["doc-id"]

Для объявления поддерживаемых форматов появился заголовок ответа Accept-Query:

HTTP/1.1 200 OKAccept-Query: application/json, "application/jsonpath"

С его помощью ресурс может сообщить клиенту: QUERY здесь поддерживается, а тело запроса может быть в таких‑то типах данных. Это полезно для клиентов, документации и диагностики. Важно, что Accept-Query не заменяет Accept: первый описывает формат запроса, второй — формат ответа.

Location и Content‑Location

У QUERY есть интересная деталь: сервер может назначить URI результату запроса или самому запросу.

Допустим, клиент отправляет:

QUERY /contacts HTTP/1.1Host: api.example.comContent-Type: application/jsonAccept: application/json{  "select": ["surname", "given_name", "email"],  "limit": 10,  "match": "email=*@example.*"}

Сервер может ответить так:

HTTP/1.1 200 OKContent-Type: application/jsonContent-Location: /contacts/stored-results/17Location: /contacts/stored-queries/42[  {    "surname": "Smith",    "given_name": "John",    "email": "smith@example.org"  }]

Content-Location здесь указывает на ресурс, соответствующий возвращённому представлению. Например, на сохранённый результат:

GET /contacts/stored-results/17 HTTP/1.1Host: api.example.comAccept: application/json

Location может указывать на ресурс, который представляет сам запрос. Клиент сможет повторить его через обычный GET, не отправляя тело заново:

GET /contacts/stored-queries/42 HTTP/1.1Host: api.example.comAccept: application/json

Для отчётов, аналитических интерфейсов и сложных фильтров это практичная модель. Пользователь собрал фильтр, сервер сохранил его как отдельный ресурс, ссылку можно передать коллеге или использовать повторно. При этом нужно заранее решить, что именно сохраняется: снимок результата на момент выполнения или формула запроса, которая при следующем обращении даст актуальные данные.

Кэширование

RFC 10008 описывает ответы на QUERY как кэшируемые. Но из этого не следует, что существующие CDN, обратные прокси и браузеры сразу начнут эффективно кэшировать такие запросы.

Для GET ключ кэша обычно строится вокруг URI и заголовков, влияющих на представление. Для QUERY одного URI недостаточно:

QUERY /orders HTTP/1.1Content-Type: application/json{"status":"paid"}
QUERY /orders HTTP/1.1Content-Type: application/json{"status":"cancelled"}

URI одинаковый, тело разное, результат тоже должен быть разным. Поэтому ключ кэша обязан учитывать содержимое запроса и связанные метаданные: Content-Type, кодирование содержимого и другие параметры, которые влияют на обработку.

Отдельная тема — нормализация. Эти два JSON‑документа могут иметь одинаковый смысл для приложения:

{"status":"paid","limit":10}
{  "limit": 10,  "status": "paid"}

По байтам они разные. По смыслу конкретного API — возможно, одинаковые. RFC допускает нормализацию для построения ключа кэша, но это требует знания формата и правил конкретного ресурса. Ошибка в нормализации может привести к выдаче чужого или неверного ответа.

Если результат запроса зависит от того, кто именно его отправил, какие у этого пользователя права доступа и какие данные ему вообще разрешено видеть, кэширование QUERY надо проектировать так же аккуратно, как кэширование GET с авторизацией. Новый метод не отменяет Cache-Control, Vary и обычную проверку прав доступа.

Практичный вариант для некоторых систем: первый запрос выполнить через QUERY, получить Location для эквивалентного ресурса, а последующие обращения делать через GET. Это упрощает жизнь кэшу и клиентам, если такая модель подходит приложению.

Логирование и чувствительные данные

Перенос параметров из URI в тело запроса снижает вероятность, что они случайно окажутся в журналах доступа. Но тело запроса не становится защищённым местом.

Содержимое QUERY может попасть в:

  • журналы приложения;

  • отладочные журналы;

  • APM;

  • распределённую трассировку;

  • WAF;

  • API‑шлюз;

  • отчёты об исключениях;

  • тестовые дампы.

Поэтому в QUERY не стоит передавать секреты, токены, приватные ключи и другие данные, которые не должны проходить через инфраструктурные журналы.

Если сервер создаёт URI для сохранённого запроса или результата, этот URI тоже не должен раскрывать чувствительные параметры.

Плохой вариант:

Location: /stored-queries/email=ivan@example.com&passport=1234567890

Лучше использовать непрозрачный идентификатор:

Location: /stored-queries/42f4b0c9

Доступ к такому ресурсу должен проверяться так же строго, как доступ к исходным данным.

CORS и браузеры

Для браузерных приложений есть отдельный момент: QUERY не входит в CORS‑safelisted methods. Поэтому межсайтовый запрос из браузера потребует предварительный OPTIONS.

Браузер сначала отправит:

OPTIONS /orders HTTP/1.1Origin: https://frontend.example.comAccess-Control-Request-Method: QUERYAccess-Control-Request-Headers: content-type

Сервер должен явно разрешить метод:

HTTP/1.1 204 No ContentAccess-Control-Allow-Origin: https://frontend.example.comAccess-Control-Allow-Methods: GET, POST, QUERY, OPTIONSAccess-Control-Allow-Headers: content-type

Для межсерверных API это обычно несущественно. Для SPA и публичных браузерных API это надо проверять до выката, иначе новая семантика упрётся в настройки CORS.

Лимиты размера остаются

QUERY не означает, что теперь можно отправлять запросы любого размера. Он убирает необходимость помещать сложные параметры в URI, но тело запроса всё равно ограничивается настройками клиента, прокси, WAF, API‑шлюза, сервера приложений, фреймворка и самого приложения.

У больших запросов остаются обычные эксплуатационные вопросы:

  • таймауты;

  • ограничение частоты запросов;

  • стоимость выполнения;

  • нагрузка на базу данных или поисковый движок;

  • лимиты памяти;

  • сложность диагностики;

  • стоимость кэширования.

Корректная формулировка такая: QUERY переносит данные запроса из URI в тело HTTP‑сообщения, где с ними обычно удобнее работать и где проще задавать контролируемые лимиты.

Где QUERY подходит

QUERY стоит рассматривать для операций, которые одновременно:

  • читают данные и не должны менять состояние целевого ресурса;

  • требуют сложного или объёмного ввода;

  • выигрывают от явной HTTP‑семантики безопасного и идемпотентного метода;

  • плохо укладываются в обычную строку запроса URI.

Типичные примеры:

  • сложный поиск;

  • аналитические отчёты;

  • фильтрация по вложенным условиям;

  • запросы к документным коллекциям;

  • JSONPath‑ или XPath‑подобные запросы;

  • GraphQL‑подобные выборки, если конкретная инфраструктура поддерживает такой способ;

  • API, где сейчас есть POST /search, хотя операция фактически только читает данные.

Пример отчёта:

QUERY /reports/revenue HTTP/1.1Host: api.example.comContent-Type: application/jsonAccept: text/csv{  "group_by": ["month", "country"],  "metrics": ["gross_revenue", "refunds", "net_revenue"],  "period": {    "from": "2026-01-01",    "to": "2026-06-30"  }}

Здесь GET с параметрами быстро превратился бы в длинный плохо читаемый URI, а POST скрыл бы от инфраструктуры тот факт, что операция не должна менять состояние ресурса.

Где QUERY не нужен

Для простых случаев лучше оставить привычные методы.

Получение ресурса по идентификатору:

GET /users/123 HTTP/1.1Host: api.example.com

Список с несколькими параметрами:

GET /orders?status=paid&limit=50 HTTP/1.1Host: api.example.com

Создание ресурса:

POST /orders HTTP/1.1Host: api.example.comContent-Type: application/json{  "user_id": 123,  "items": []}

Частичное изменение:

PATCH /orders/123 HTTP/1.1Host: api.example.comContent-Type: application/json{  "status": "cancelled"}

Удаление:

DELETE /orders/123 HTTP/1.1Host: api.example.com

Если операция меняет состояние, запускает задачу, создаёт экспорт, отправляет письмо или оставляет бизнес‑эффект, QUERY для неё не подходит. В таких случаях лучше честно использовать POST, PATCH, PUT или другой метод с подходящей семантикой.

Что должно измениться вокруг QUERY

Отправить QUERY через curl несложно. Daniel Stenberg, автор curl, отдельно разбирал это в заметке QUERY with curl:

curl -X QUERY \  -H 'Content-Type: application/json' \  -H 'Accept: application/json' \  --data '{"status":"paid","limit":100}' \  https://api.example.com/orders

Но новый HTTP‑метод становится практически полезным не в момент публикации RFC. Его должны начать нормально пропускать, понимать и диагностировать все участники цепочки. В этом смысле QUERY сейчас находится в обычной для нового стандарта ситуации: спецификация уже есть, а массовая поддержка в инструментах будет появляться постепенно.

Первый слой — клиенты.

Низкоуровневые HTTP‑клиенты часто умеют отправлять произвольный метод: строка метода для них просто часть HTTP‑запроса. Но поверх них обычно есть более удобные обёртки: SDK, REST‑клиенты, клиентский код, сгенерированный по OpenAPI‑описанию, механизмы повторов, политики идемпотентности, типизированные перечисления методов. В таких местах QUERY может не пройти проверку, не попасть в список разрешённых методов или не получить правильное поведение для повторов.

С браузерами тоже есть нюанс. Есть стандарт Fetch — это спецификация, по которой браузеры реализуют fetch(), сетевые запросы из JavaScript, CORS и связанные с этим правила. В нём запрещёнными методами считаются CONNECT, TRACE и TRACK; произвольный метод вроде QUERY сам по себе в этот список не попадает. Но QUERY не входит в список простых CORS‑методов (GET, HEAD, POST), поэтому межсайтовый запрос через fetch потребует предварительный OPTIONS. Обычная HTML‑форма метод QUERY не отправит, навигация по ссылке тоже. Кэш браузера, DevTools, Service Worker и клиентские библиотеки надо проверять отдельно, а не считать поддержку полной только потому, что один fetch в одном браузере отправил запрос.

На июль 2026 года публично объявленных сроков поддержки QUERY в Chrome, Firefox и Safari нет. Обсуждение интеграции в Fetch уже началось, но до стадии «это обычный браузерный метод, который везде работает предсказуемо» ещё нужно пройти путь через спецификации, реализации и совместимость с промежуточной инфраструктурой.

Второй слой — серверное приложение.

Маршрутизатор фреймворка должен разрешить новый метод, документация должна уметь его описать, тестовый клиент должен уметь его вызвать, промежуточный обработчик авторизации должен применять к нему правильные правила. Если в коде где‑то есть проверка вида «разрешены только GET, POST, PUT, PATCH, DELETE», QUERY остановится внутри приложения.

Третий слой — промежуточная инфраструктура.

Здесь список длиннее:

  • CDN и обратные прокси должны пропускать метод и корректно работать с телом запроса;

  • кэши должны строить ключ с учётом тела и Content-Type, иначе кэширование будет небезопасным;

  • WAF должен понимать, что тело у QUERY ожидаемо, и применять к нему правила проверки;

  • API‑шлюз должен разрешить метод в маршрутах и политиках безопасности;

  • балансировщик и ingress‑контроллер не должны отбрасывать запрос как неизвестный;

  • корпоративный прокси, например Squid, может иметь правила доступа по HTTP‑методу;

  • журналы и трассировка должны показывать метод, тело, статус и причину отказа достаточно подробно для диагностики.

С Squid и похожими прокси важно не гадать по названию продукта, а смотреть конкретную конфигурацию. В Squid есть ACL по методу и правила http_access; в одних установках неизвестный метод может пройти, в других корпоративная политика разрешит только привычный набор. С WAF ситуация похожая: часть правил завязана на список методов, часть — на ожидание тела только у POST, PUT и PATCH, часть — на сигнатуры конкретных API.

Четвёртый слой — эксплуатационные соглашения.

Нужно решить, как QUERY участвует в:

  • повторных попытках после сетевых ошибок;

  • ограничении частоты запросов;

  • учёте стоимости тяжёлых поисковых операций;

  • журналировании тела запроса;

  • маскировании чувствительных данных;

  • мониторинге ошибок 400, 405, 415, 422;

  • схемах OpenAPI и автогенерации клиентов;

  • интеграционных тестах через реальные прокси и шлюзы.

Во многих системах есть явный список разрешённых HTTP‑методов. Если где‑то перечислены только GET, POST, PUT, PATCH, DELETE и OPTIONS, новый метод может получить 405 Method Not Allowed или быть заблокирован ещё до приложения.

Поэтому разумный путь внедрения выглядит так:

  1. Выбрать операции, которые действительно являются безопасными и идемпотентными.

  2. Описать формат тела запроса: JSON Schema, JSONPath, application/x-www-form-urlencoded или собственный тип данных.

  3. Добавить QUERY рядом с существующим POST /search, если такой адрес уже есть.

  4. Явно публиковать поддержку метода и формата:

Allow: GET, QUERY, OPTIONS, HEADAccept-Query: application/json
  1. Проверить всю инфраструктуру: шлюзы, WAF, прокси, CORS, журналы, мониторинг и тесты.

  2. Отдельно спроектировать кэширование, Location, Content-Location и сохранённые запросы, если они нужны.

На первом этапе POST /orders/search и QUERY /orders могут использовать одну серверную логику. Разница будет в HTTP‑семантике и в том, как эту операцию видят клиенты и промежуточные компоненты.

Итог

Теперь у нас есть QUERY — стандартизованный HTTP‑метод для безопасных и идемпотентных запросов с телом.

Он полезен там, где GET со строкой запроса становится громоздким, GET с телом плохо поддерживается инфраструктурой, а POST /search не выражает смысл операции на уровне HTTP.

Существующие API не нужно срочно переписывать. Но для новых поисковых, аналитических и документных API QUERY даёт более точную модель:

QUERY /resourceContent-Type: application/json{  "...": "сложный запрос на чтение"}

Старые способы остаются нормальными там, где они просты и понятны. Новый метод нужен для случаев, где параметры запроса уже стали полноценным структурированным вводом, а операция всё ещё должна оставаться чтением.

тот же плакатик из КДПВ

ссылка на оригинал статьи https://habr.com/ru/articles/1055310/