The HTTP QUERY Method: A Safe, Idempotent Request With a Body
by Maven Team, Software Development
For as long as most of us have been building APIs, sending a complex query to a server has meant picking the lesser of two awkward options. You either cram everything into a GET URL and run into length and encoding limits, or you reach for POST and quietly give up on the things that make GET good — caching, idempotency, and a clear "this request changes nothing" contract.
The IETF has now standardised a third option. The HTTP QUERY method is a new, safe and idempotent method that carries a request body, defined specifically for querying resources. As of June 2026 it is no longer a working-group draft: it was published as RFC 10008, a Standards Track document. This article walks through what it is, the problem it solves, how it works, and the practical implications for the APIs we build.
The problem: GET and POST both fall short
The HTTP semantics specification (RFC 9110) gives every method two important properties: whether it is safe (the client does not expect or request any change to the server's state) and whether it is idempotent (sending the same request twice has the same effect as sending it once). These properties are not bureaucratic trivia. They are the contract that caches, proxies, retry logic, and crawlers rely on to behave correctly.
GET is both safe and idempotent, which is exactly what you want for a read. The trouble is that GET has no reliable way to carry a request body. The specification has long discouraged bodies on GET, and in practice intermediaries, servers, and client libraries will strip, ignore, or reject them. So everything a GET request needs has to live in the URL. That is fine for /products?category=shoes, and a real problem the moment your query is a nested filter object, a GraphQL document, a SQL-like expression, or a geospatial polygon. URLs have practical length limits, every value has to be percent-encoded, and a long query string is a poor fit for structured data.
POST solves the body problem and nothing else. It can carry any payload you like, but POST is — by definition — neither safe nor idempotent. A cache will not store the response. A proxy will not assume the request can be safely retried. Anyone reading your access logs cannot tell a genuine mutation apart from a search that happened to need a body. You end up using a method that signals "this writes data" to perform an operation that reads data, and every layer of the stack that pays attention to method semantics is now misinformed.
This is the gap QUERY fills. It gives you a body and the safe, idempotent, cacheable semantics of a read.
What QUERY actually is
QUERY requests that the target resource process the content of the request body and return the result of that processing. Semantically it sits very close to POST in shape — a method with a meaningful request body — but with two crucial differences declared up front:
- It is safe. The client is not asking for any change to the resource's state. This tells caches, crawlers, and security tooling that the request is a read.
- It is idempotent. Because the result depends only on the request content, the request can be automatically retried or repeated without concern for partial state changes. A proxy that loses a connection mid-flight can resend it; a client library can retry on a timeout without the usual "did that actually go through?" anxiety that surrounds
POST.
The request body carries the query, and a Content-Type header is mandatory so the server knows how to interpret it. The format is entirely up to your API: application/json, application/sql, application/graphql, application/x-www-form-urlencoded, or anything else you define. Content negotiation works exactly as it does elsewhere in HTTP — the client sends an Accept header to ask for the response format it wants, and the server responds accordingly.
A concrete example
Here is the example from the specification's appendix (trimmed only of its date headers). A client queries a contacts resource, sending the query parameters in the body as form-encoded data and asking for JSON back:
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=%22email=\*@example.\*%22
The server processes the query and returns the result:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Location: /contacts/stored-results/17
Location: /contacts/stored-queries/42
[
{ "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" }
]
Nothing here is exotic. That is rather the point — it looks like a POST you have written a thousand times, except the method name now tells every intermediary the truth about what the request does. The two extra response headers, Content-Location and Location, are worth dwelling on, because they are how QUERY connects a one-off body-carrying request back to the cacheable, linkable world of GET.
Responses, Content-Location, and retrieving results later
A 200 OK carries the query result directly in the response body, as above. The specification also defines how a server can point at the result as an addressable resource.
If the server sets a Content-Location header, it identifies a resource that corresponds to the results of the operation. A client can then issue an ordinary GET to that URI to retrieve the same results later — useful when you want a cacheable, shareable, bookmarkable link to a result set that was originally produced by a body-carrying query.
A Location header serves a related but distinct purpose: it indicates a URI for an equivalent resource, so a client can repeat the same query later without resending the query body. The distinction matters. Content-Location says "here is where the results live"; Location says "here is a URL that re-runs this query for you".
Cacheability
This is where QUERY earns its keep over POST. The specification states that the response to a QUERY request is cacheable, and it defines how: the cache key must incorporate the request content and related metadata, not just the request URI. That is the missing piece that has always made caching a POST impractical — two POSTs to the same URL can mean entirely different things, so the URL alone is a useless cache key. By folding the body into the key, a cache can safely store and reuse the response to a specific query.
Caches are allowed to normalise semantically insignificant differences in the request content before computing the key — collapsing whitespace, say, so that two formattings of the same JSON query hit the same cache entry. A client that needs to prevent this can send the no-transform cache directive, though the RFC is explicit that the directive is only advisory.
In practice you should treat QUERY caching the way you treat any HTTP caching: it is opt-in via the usual response headers (Cache-Control, ETag, and friends). The method makes caching possible and gives caches a well-defined key; your headers still decide what actually gets stored and for how long.
Practical implications for developers
The method is newly standardised, so the honest framing is "emerging", not "ubiquitous". Here is how we are thinking about it.
Browser and client support is the current gatekeeper. A brand-new method takes time to land in browsers, HTTP client libraries, server frameworks, proxies, and CDNs. The RFC flags one browser-specific consequence directly: because QUERY is not a CORS-safelisted method, a cross-origin QUERY from a browser triggers a preflight OPTIONS request before the query itself is sent. Until support is broad, server-to-server and internal API traffic — where you control both ends — is the natural first home for QUERY, rather than public browser-facing endpoints.
It is a clean answer to a real pain point. Anyone who has built a search or reporting endpoint has felt the GET-versus-POST tension directly. Teams that adopted POST /search to escape URL limits gave up caching and idempotency to do it; teams that stayed with GET ended up encoding JSON into query strings. GraphQL endpoints, which famously tunnel everything through POST and forfeit HTTP caching, are an obvious candidate to benefit. QUERY lets you keep the body and get the read semantics back.
Design for graceful degradation. Because support is still rolling out, a real-world API that wants to use QUERY today should be prepared to also accept the equivalent POST (or GET) as a fallback, and advertise QUERY support so capable clients can prefer it. This is the same incremental-adoption posture that served well with earlier HTTP additions: offer the better path, keep the old one working, and let clients upgrade when they can.
Watch your intermediaries. Load balancers, API gateways, WAFs, and CDNs may not yet recognise QUERY, and some default to blocking unknown methods. Before relying on it in production, confirm that everything between your client and your origin will pass a QUERY request through — and, if you want the caching benefit, that your CDN understands how to key on the body.
Adopting QUERY today, in code
The method is standardised, but the toolchain is still catching up — so the realistic pattern right now is to prefer QUERY and fall back to POST. Here is a small client helper that does exactly that. Modern browsers' fetch() will send a custom method like QUERY; the fallback covers the servers, proxies, and CDNs on the path that do not yet route it.
// Prefer QUERY, fall back to POST. Because QUERY is safe and idempotent,
// there is no harm in retrying the same request a second way.
async function query(url: string, body: unknown) {
const init = {
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(body),
}
try {
const res = await fetch(url, { method: 'QUERY', ...init })
// 405/501 means something on the path does not understand QUERY.
// Any other status is a real answer — return it rather than retrying.
if (res.status !== 405 && res.status !== 501) return res
} catch {
// Some intermediaries reject an unknown method at the connection layer.
}
return fetch(url, { method: 'POST', ...init })
}
In production you would cache the outcome of that first attempt rather than probing on every call — once you know an endpoint speaks QUERY, keep using it.
The server side is where support is thinnest, and it is worth being concrete about the limits. This blog runs on Next.js, and Next.js route handlers only dispatch the standard method set — GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS. You cannot export a QUERY handler from a route.ts file today; a QUERY request to one returns 405. Node's built-in HTTP server has likewise historically rejected unknown methods. So on this stack, the endpoint your client falls back to is an ordinary POST handler:
// app/api/contacts/route.ts
import { type NextRequest } from 'next/server'
// A read-only lookup — no state changes. That is the "safe" half of the
// contract, whether the request arrives as QUERY or as the POST fallback.
async function runQuery(criteria: {
select?: string[]
limit?: number
match?: string
}) {
// ...perform the read and return the matching records...
return []
}
export async function POST(request: NextRequest) {
const results = await runQuery(await request.json())
return Response.json(results)
}
On runtimes built directly around the Web Request API — Deno, Bun, Cloudflare Workers — request.method reports whatever method actually arrived, including "QUERY", so you can branch on it and accept both in one place:
async function handler(request: Request): Promise<Response> {
if (request.method === 'QUERY' || request.method === 'POST') {
const results = await runQuery(await request.json())
return Response.json(results)
}
return new Response('Method Not Allowed', {
status: 405,
headers: { Allow: 'QUERY, POST' },
})
}
Whether such a request reaches your code at all still depends on every hop in front of it — load balancer, gateway, CDN — passing the method through. Keeping the query logic in one function, as runQuery is here, means that as your framework, runtime, and CDN catch up, adopting QUERY becomes a routing change rather than a rewrite.
Where this lands
The HTTP QUERY method closes a gap that has quietly shaped API design for years. The instinct to reach for POST whenever a query outgrew the URL was always a compromise — trading away caching and idempotency for the ability to send a body. QUERY removes that trade-off. You get a request body, content negotiation, well-defined cacheability, and an honest, safe, idempotent contract that the whole HTTP stack can reason about.
Now that it is an RFC rather than a draft, the specification itself is settled. What remains is the rollout — the long tail of clients, servers, and intermediaries learning the new method. We would not rip out a working POST /search endpoint tomorrow. But for new internal APIs, for query-heavy services where caching matters, and anywhere the GET-versus-POST compromise has bothered you, QUERY is now a standards-backed option worth designing for.