C# Short Posts · RFC 10008 · июнь 2026
HTTP QUERY
GET с телом,
который мы заслужили
Новый HTTP-метод: семантика GET, тело как у POST.
Что это, зачем это и как это уже работает в .NET 10.
Куда девать сложный поиск?
GET /search?…
- Тело класть нельзя — семантика не определена
- Фильтры утрамбовываются в query string, пока URL не лопнет
- Массивы и вложенные структуры — боль и «?ids=1,2,3»
- Зато safe, идемпотентный, кэшируемый
POST /search
- Тело есть — любой JSON-фильтр
- Не safe и не идемпотентный
- Ретраить нельзя: клиент не знает, что повтор безопасен
- Кэшировать нельзя
Двадцать лет выбираем меньшее из двух зол — и делаем вид, что POST /search так и задумано.
QUERY = семантика GET + тело POST
QUERY /search
- Safe — не меняет состояние сервера
- Идемпотентный — клиент вправе повторить при таймауте, не боясь что-то сломать
- Тело запроса — JSON-фильтр любой сложности
- Кэшируемый — по спеке да, инфраструктура догоняет
RFC 10008, Proposed Standard, июнь 2026. Больше не драфт — официальный стандарт IETF.
Что завезли в .NET 10
Есть
- HttpMethod.Query в HttpClient — и ретраи из коробки, метод-то идемпотентный
- HttpMethods.Query и IsQuery() в ASP.NET Core
- Kestrel парсит QUERY без плясок
Вырезали на API-ревью
- MapQuery() для minimal API
- [HttpQuery] для контроллеров
- Обещают посмотреть в .NET 11. Пока — extension в три строки
Сервер: minimal API
те самые три строки вместо MapQuery()
public static IEndpointConventionBuilder MapQuery(
this IEndpointRouteBuilder endpoints, string pattern, Delegate handler) =>
endpoints.MapMethods(pattern, [HttpMethods.Query], handler);
один хендлер — оба метода
app.MapQuery("/products/search", SearchAsync);
app.MapPost ("/products/search", SearchAsync); // fallback для тех, кто за прокси
static async Task<IResult> SearchAsync(HttpRequest request)
{
var filter = await request.ReadFromJsonAsync<ProductFilter>();
return Results.Ok(await catalog.SearchAsync(filter));
}
Клиент: HttpClient и fetch
C# — HttpMethod.Query уже в коробке
using var request = new HttpRequestMessage(HttpMethod.Query, "/products/search")
{
Content = JsonContent.Create(filter)
};
var response = await httpClient.SendAsync(request);
js — fetch пропускает QUERY как обычный метод
const res = await fetch('/products/search', {
method: 'QUERY', // запрещены только CONNECT / TRACE / TRACK
body: JSON.stringify(filter),
});
Где споткнёшься
- 01CORS-preflight — всегда. QUERY не в safelist: кросс-доменный запрос едет с OPTIONS, а сервер обязан вернуть Access-Control-Allow-Methods: QUERY
- 02Чужие middleboxes. Корпоративные прокси и WAF'ы режут незнакомые методы по allowlist — браузер отправит, а по дороге срежут
- 03Кэша пока нет. Главный бонус спеки браузеры и CDN ещё не реализовали — сейчас выигрыш в семантике и ретраях
- 04Тулинг догоняет. Swagger, Postman, тестовые клиенты — не все знают метод, проверь свой стек на деве
Fallback на POST: сам не сделается
let queryBroken = false; // не прошло раз — не пробуем до конца сессии
if (!queryBroken) {
try {
const res = await fetch(url, { method: 'QUERY', body });
if (res.status !== 405) return res; // 400/422 — плохой фильтр, POST не спасёт
} catch { /* сеть или прокси срезали метод */ }
queryBroken = true;
}
return fetch(url, { method: 'POST', body }); // тот же эндпоинт, та же логика
Фолбэчим только на 405 и сетевые ошибки — и запоминаем на сессию, чтобы не платить лишним запросом каждый раз.
Итог
- Бэкенд-ту-бэкенд — переводи хоть сегодня: ни CORS, ни чужих прокси, а HttpClient сам ретраит идемпотентный метод
- Публичный фронт — можно, но только с fallback'ом на POST
- Кэширование — подождём инфраструктуру, спека уже готова
- POST /search всегда был костылём — просто мы к нему привыкли
RFC 10008 · datatracker.ietf.org
демо: csharpshortpostsexamples/HttpQueryExample · dotnet run → localhost:5177