For as long as most of us have been writing APIs, there has been one awkward question with no good answer: where do I put a complex search query?
In June 2026 the IETF finally answered it. RFC 10008 defines a new HTTP method, QUERY: safe and idempotent like GET, cacheable like GET, but with a request body like POST. It's the first new general-purpose method HTTP has gained in about twenty years.
The problem: GET vs POST was always a bad trade
Say you're building a product search endpoint. You have two classic options, and both are compromises.
The first is GET with a query string:
GET /products?category=books&maxPrice=30&sort=rating&fields=title,price HTTP/1.1
You get safety, idempotency, caching, bookmarks. You also get practical URL length limits; servers and proxies commonly cap URLs somewhere between 2 KB and 8 KB, which starts to hurt once your filter UI grows or you need to pass a long list of IDs. The whole query also lands in access logs and browser history, bad news if it contains personal data. And no, you can't just put a body on the GET instead: the spec gives a GET body no defined semantics, so servers and intermediaries do unpredictable things with it.
The second option is POST with a body:
POST /products/search HTTP/1.1
Content-Type: application/json
{ "category": "books", "maxPrice": 30, "sort": "rating" }
Now there are no size limits and nothing sensitive in the URL. But POST is neither safe nor idempotent by definition. Caches have to assume it changes state, so responses can't be reused. A client that hits a network failure can't retry automatically, because it has no way to know whether that's safe. And the message itself no longer says "this is just a read".
Every REST API that grew a /search POST endpoint made this trade. QUERY exists so you don't have to.
Meet QUERY
From RFC 10008, the QUERY method:
"requests that the request target process the enclosed content in a safe and idempotent manner and then respond with the result of that processing."
Unpacked: safe means the client neither requests nor expects any state change on the target resource. It's a read. Idempotent means it can be repeated or retried, say after a connection failure, with no concern about partial state changes. Cacheable means a cache MAY use the response to satisfy later equivalent QUERY requests. And unlike GET, the query itself travels in the request body, in whatever media type the resource supports.
Here is a minimal exchange (adapted from the RFC's own example):
QUERY /contacts HTTP/1.1
Host: example.org
Content-Type: application/x-www-form-urlencoded
Accept: application/json
select=surname,givenname,email&limit=10&match="email=*@example.*"
HTTP/1.1 200 OK
Content-Type: application/json
[
{ "surname": "Smith", "givenname": "John", "email": "smith@example.org" },
{ "surname": "Jones", "givenname": "Sally", "email": "sally.jones@example.com" },
{ "surname": "Dubois", "givenname": "Camille","email": "camille.dubois@example.net" }
]
The body isn't limited to form encoding. The RFC's examples use application/x-www-form-urlencoded, JSONPath, SQL and even XSLT as query languages. The media type defines what the query means; the resource defines what it's evaluated against.
GET vs QUERY vs POST at a glance
| Property | GET | QUERY | POST |
|---|---|---|---|
| Safe | yes | yes | potentially no |
| Idempotent | yes | yes | potentially no |
| Request body | no defined semantics | expected | expected |
| Cacheable response | yes | yes | only for future GET/HEAD |
| Automatic retry OK | yes | yes | no |
| URI for the query itself | yes, by definition | optional (Location) |
no |
| URI for the result | optional (Content-Location) |
optional (Content-Location) |
optional (Content-Location) |
The rules of the road
A few things the RFC is strict about.
Content-Type is mandatory. Servers must fail a QUERY request whose Content-Type is missing or inconsistent with the actual content. No content sniffing.
Error signaling is well defined:
| Situation | Status |
|---|---|
| Missing media type | 400 Bad Request |
| Media type not supported for queries | 415 Unsupported Media Type |
| Content doesn't match its declared type | 400 Bad Request |
| Query parses but is semantically invalid | 422 Unprocessable Content |
| Server can't produce a response type the client accepts | 406 Not Acceptable |
| Server doesn't support QUERY at all |
405 Method Not Allowed (with an Allow header) |
And the URI's query string still exists. QUERY /contacts?active=true is legal; the query component still participates in identifying the resource, while the body carries the query content. How the two combine is up to the resource.
Discovery: Allow and the new Accept-Query header
RFC 10008 also registers a new response header, Accept-Query, which advertises the media types a resource accepts for QUERY bodies. It uses Structured Fields syntax:
HEAD /contacts HTTP/1.1
Host: example.org
HTTP/1.1 200 OK
Accept-Query: application/x-www-form-urlencoded, "application/sql"
There are two more discovery paths. Send OPTIONS /contacts and look for QUERY in the Allow response header. Or just try it: an unsupported method gets you a 405 with Allow, and an unsupported query format gets you a 415, ideally with Accept-Query telling you what would have worked.
One subtlety: Accept-Query applies to every URI on the server that shares the same path. The URI's query component is ignored for its scope.
Caching: the killer feature
This is the part that makes QUERY an actual improvement over tunneling reads through POST.
Because QUERY is safe, its responses are cacheable. Unlike GET, though, the URL alone isn't enough to identify a response, so the RFC requires:
The cache key for a QUERY request must incorporate the request content and related metadata (like
Content-Type).
In other words: same URL plus same query body (plus the relevant headers) equals a cache hit. A CDN or shared proxy can serve your repeated dashboard query without ever touching the origin, which was structurally impossible with POST-based search.
Caches are also allowed to normalize the request content when building the key, for example by stripping content encodings or applying known conventions of the format (JSON's insignificant whitespace, say), so trivially different bodies can still hit the same cache entry. Normalization only ever affects the cache key; the request itself is left untouched. The RFC warns caches to be conservative here, because bad normalization means wrong responses served. Clients can send Cache-Control: no-transform to discourage it, though the directive is advisory.
The practical takeaway for API authors: emit the same caching headers on QUERY responses that you would on GET (Cache-Control, ETag, Last-Modified, Vary) and you get HTTP caching on complex searches for free as caches roll out support.
Location, Content-Location, and the "equivalent resource"
The RFC introduces a concept called the equivalent resource: the hypothetical resource that would answer a GET with the results of this exact query. Servers may give that resource a real URI, and two response headers let them tell you about it.
Location on a 2xx response means: you can GET this URI later to re-run the query without resending the body. Think saved queries. Content-Location means: you can GET this URI to retrieve this specific result snapshot, which may be temporary.
HTTP/1.1 200 OK
Content-Type: application/json
Location: /contacts/stored-queries/42 (re-runs the query)
Content-Location: /contacts/stored-results/17 (this specific result set)
[ ...results... ]
A server can even respond 303 See Other with a Location, meaning the answer to your query is available as a plain GET over there. That's a useful pattern for expensive queries: QUERY once, then poll or share a cheap, cacheable GET URL.
Conditional requests work too
Because QUERY is a read, the whole conditional-request machinery from RFC 9110 applies. Send If-None-Match or If-Modified-Since with a repeated QUERY and the server can answer 304 Not Modified, skipping both the query execution and the response body. For heavy analytical queries over slowly changing data, that's a big deal.
Redirects
Worth knowing because this differs from POST's legacy behavior. On 301, 308, 302 and 307, the client resends the QUERY, body included, to the new URI; the old browser habit of demoting redirected POSTs to GETs explicitly does not apply. On 303 See Other, the client performs a GET on the Location URI instead.
Using it today
QUERY is just an HTTP method token, and most HTTP stacks can carry it already. What varies is framework sugar and intermediary awareness.
curl
curl --request QUERY https://api.example.org/products \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data '{ "category": "books", "maxPrice": 30 }'
JavaScript (fetch)
The Fetch Standard forbids only CONNECT, TRACE, and TRACK, so QUERY is allowed:
const res = await fetch("/products", {
method: "QUERY", // uppercase: only the classic six methods are auto-normalized
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ category: "books", maxPrice: 30 }),
});
const products = await res.json();
Two caveats. Cross-origin QUERY always triggers a CORS preflight, since it's not a safelisted method. And browser and runtime support has been landing gradually through 2026, so verify in your targets before shipping.
.NET 10 / ASP.NET Core 10
.NET 10 shipped support on both sides:
// client
using var request = new HttpRequestMessage(HttpMethod.Query, "https://api.example.org/products")
{
Content = JsonContent.Create(new ProductFilter("books", MaxPrice: 30))
};
using var response = await httpClient.SendAsync(request);
// server (minimal APIs; use MapMethods, there's no MapQuery yet)
app.MapMethods("/products", new[] { HttpMethods.Query }, async (HttpContext ctx, ICatalog catalog) =>
{
var filter = await ctx.Request.ReadFromJsonAsync<ProductFilter>();
return filter is null ? Results.BadRequest() : Results.Ok(await catalog.SearchAsync(filter));
});
Node.js / Express
Routing frameworks generally expose custom methods once the underlying HTTP parser accepts them. Check your Node version's method allowlist, then route with something like:
app.all("/products", (req, res, next) => {
if (req.method !== "QUERY") return next();
// req body = the query
});
Graceful fallback
During the transition, attempt QUERY and fall back to your existing POST search endpoint when you get a 405 Method Not Allowed or a blocked request. Both can share the same handler server-side.
Gotchas and sharp edges
- WAFs, load balancers and API gateways with method allowlists may reject or mangle QUERY. Audit your edge before rolling it out.
- The semantics allow shared caches and CDNs to cache QUERY, but the products need body-aware cache keys first. Treat CDN-level QUERY caching as a roadmap item for now.
- Every cross-origin browser use pays a CORS preflight. Cache it with
Access-Control-Max-Age. - OpenAPI can't describe QUERY endpoints yet; ASP.NET Core 10, for instance, simply omits them from generated specs. Document them manually for now.
- Moving query parameters out of URLs is a privacy win, since less leaks into logs and history. But if your server mints
LocationorContent-LocationURIs for queries, the RFC says those URIs SHOULD NOT embed sensitive parts of the query content. Don't undo the win. - Idempotence is a promise you make. Marking an endpoint QUERY tells every client and intermediary "retry me freely, cache me". If the handler secretly writes state, you'll get all the classic broken-cache and double-execution bugs. Reads only.
When to reach for QUERY
Use it when a read doesn't fit comfortably in a URL:
- complex filter and search endpoints (the
/searchPOST you already have) - queries written in a real query language (JSONPath, SQL subsets, GraphQL documents)
- lookups with large ID lists ("give me these 5,000 SKUs")
- reads whose parameters are sensitive and shouldn't live in URLs and logs
Keep plain GET for anything that fits in a URL; bookmarks and universal caching are still unbeatable. Keep POST for what it always meant: requests that change state.
Wrap-up
QUERY closes a twenty-year-old gap in HTTP's vocabulary: a read with a body. The parts beyond that headline are what make it worth adopting, especially the body-aware caching and the stored-query pattern via Location.
The ecosystem is still catching up. Middleboxes and CDN cache keys will take a while, and OpenAPI has no way to describe QUERY endpoints yet. But the standard is done and the first frameworks have shipped, and keeping a POST fallback around costs almost nothing. Next time you're about to add a POST /search endpoint, try QUERY first.
References
- RFC 10008 — The HTTP QUERY Method (Proposed Standard, June 2026)
- IETF datatracker history — draft-ietf-httpbis-safe-method-w-body
- RFC 9110 — HTTP Semantics (method properties, conditional requests)
-
RFC 9651 — Structured Field Values (the
Accept-Querysyntax)
Top comments (0)