For decades we've had exactly two bad options for "read something using a complex input": cram everything into a GET URL, or lie and use POST. In June 2026, the IETF standardized a real fix:QUERY method (RFC 10008). Here's what it is, why it exists, and how to actually use it.
The problem: GET and POST both fall short
Say you have a search endpoint. The natural, RESTful choice is GET:
GET /contacts?select=surname,email&match=email=*@example.*&limit=10 HTTP/1.1
Host: example.org
GET is safe, idempotent, and cacheable. Perfect until your query grows. Then you hit real walls the RFC calls out explicitly.
- URL length limits you can't predict, because the request passes through proxies, gateways, and CDNs you don't control.
- URLs get logged and bookmarked. Putting something sensitive in the URL means it lands in access logs, browser history, and referrer headers.
- Encoding overhead: nesting a rich filter object into a query string is ugly and inefficient.
So everyone reaches for the workaround: POST.
POST /contacts HTTP/1.1
Host: example.org
Content-Type: application/json
{ "match": { "email": "*@example.*" }, "select": ["surname", "email"], "limit": 10 }
This solves the size problem, but throws away the semantics. Nothing in the protocol says this POST is safe or idempotent. Caches won't cache it. Intermediaries won't retry it after a dropped connection. Every GraphQL API that sends read queries over POST lives with exactly this compromise.
The fix: QUERY
QUERY is the missing middle. It takes the request body from POST and the safe + idempotent + cacheable semantics from GET:
QUERY /contacts HTTP/1.1
Host: example.org
Content-Type: application/json
Accept: application/json
{ "match": { "email": "*@example.*" }, "select": ["surname", "email"], "limit": 10 }
The input goes in the body (any media type you like - JSON, SQL, JSONPath, XSLT, form-encoded), but because the method is defined as safe and idempotent, caches and intermediaries can treat it like GET: cache the response, and safely retry after a connection failure.
A complete request/response
The response to a successful QUERY is just the result:
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
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 clever part: turning a query into a GET-able URL
Caching a QUERY is trickier than caching a GET, because the cache key has to include the request body, not just the URL. RFC 10008 offers an elegant escape hatch: the server can hand back a URL you can GET afterward, via two different response headers.
Content-Location → a URL for this result (a snapshot):
HTTP/1.1 200 OK
Content-Type: application/json
Content-Location: /contacts/stored-results/17
Location: /contacts/stored-queries/42
GET /contacts/stored-results/17 HTTP/1.1 # fetch the same result again
Location → a URL for the query itself (re-run it later, without resending the body):
GET /contacts/stored-queries/42 HTTP/1.1 # re-runs the query, fresh results
This is the best of both worlds: send the big body once via QUERY, then switch to plain, cache-friendly GETs-even conditional ones with ETag/If-None-Match for cheap 304 Not Modified responses.
Error handling worth knowing
The RFC is specific about status codes, which makes for robust APIs:
Missing Content-Type → 400. Servers must not content-sniff.
Query format not supported → 415 (pair it with Accept-Query).
Body valid, but query unprocessable (e.g., syntactically correct SQL referencing a missing table) → 422 Unprocessable Content.
Requested response type via Accept not available → 406 Not Acceptable.
Things to watch
- CORS preflight required. QUERY isn't a CORS-safelisted method, so browsers will send an OPTIONS preflight. (Browser
fetch()support is still rolling out-check before relying on it client-side.) - Caching needs the body. A cache must read the request content to compute the key. The RFC allows normalization (e.g., +json structural normalization) to improve hit rates-but sloppy normalization can cause false-positive cache hits.
- Sensitive data in generated URLs. If the server mints a
Location/Content-LocationURL for a query that contained secrets, that URL shouldn't encode the sensitive parts (they'd end up in logs-the very thing QUERY helps you avoid). - Redirects differ from POST. Unlike POST, a
301/302on a QUERY does not get rewritten to a GET; the agent re-sends a QUERY to the new target. (303 See Other does mean "GET the result over there.") - Adoption curve. It's a Proposed Standard authored with Cloudflare and Akamai, so CDN/edge support may land before your web framework and HTTP client do. Expect a transition period.
When to use it
- Reach for QUERY when a read operation has a body that's too big, too structured, or too sensitive for a URL:
- Search/filter endpoints with rich filter objects.
- GraphQL read queries (finally, safe semantics instead of POST).
- Reporting/analytics queries (SQL, JSONPath, XSLT over a dataset).
- Any "GET with a body" you've been faking with POST.
Top comments (0)