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

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),
});

Где споткнёшься

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 и сетевые ошибки — и запоминаем на сессию, чтобы не платить лишним запросом каждый раз.

Итог

RFC 10008 · datatracker.ietf.org
демо: csharpshortpostsexamples/HttpQueryExample · dotnet run → localhost:5177

HTTP QUERY · C# Short Posts ← → · свайп · 1 / 9