DEV Community

Sanjay Ponraj
Sanjay Ponraj

Posted on

HTTP finally has a proper way to send a query: meet the QUERY method

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
Enter fullscreen mode Exit fullscreen mode

GET is safe, idempotent, and cacheable. Perfect until your query grows. Then you hit real walls the RFC calls out explicitly.

  1. URL length limits you can't predict, because the request passes through proxies, gateways, and CDNs you don't control.
  2. URLs get logged and bookmarked. Putting something sensitive in the URL means it lands in access logs, browser history, and referrer headers.
  3. 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 }
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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" }
]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

  1. 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.)
  2. 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.
  3. Sensitive data in generated URLs. If the server mints a Location/Content-Location URL 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).
  4. Redirects differ from POST. Unlike POST, a 301/302 on 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.")
  5. 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

  1. Reach for QUERY when a read operation has a body that's too big, too structured, or too sensitive for a URL:
  2. Search/filter endpoints with rich filter objects.
  3. GraphQL read queries (finally, safe semantics instead of POST).
  4. Reporting/analytics queries (SQL, JSONPath, XSLT over a dataset).
  5. Any "GET with a body" you've been faking with POST.

Top comments (0)