Saikat
← Back to blog

The New HTTP Method: QUERY

August 9, 2026·9 min read
HTTPAPI DesignBackendRFC
Share:
The New HTTP Method: QUERY

Every backend engineer has hit this wall: a search is too complex for a query string, so you stuff it into POST /search and pretend the verb is fine. It works. Caches hate it. Retries get nervous. Intermediaries have no idea you meant "read, don't write." For years that was the pragmatic answer, because HTTP had no method that was both safe and allowed to carry a body with real semantics.

As of June 2026, it does. RFC 10008 standardizes the QUERY method — a request that asks the target resource to process enclosed content in a safe and idempotent way and return the result. Same shape as POST, same safety story as GET. That gap between "I need a body" and "I promise this won't mutate anything" finally has an official verb.

The problem QUERY actually solves

The classic pattern is fine until it isn't:

GET /feed?q=foo&limit=10&sort=-published HTTP/1.1
Host: example.org

Once the filter set grows — nested boolean trees, geospatial polygons, GraphQL-sized selection sets, a blob of SQL or JSONPath — URI-encoding that into the query string starts to hurt:

  • Length limits are fuzzy. Intermediate proxies, browsers, and servers disagree on how long a URI can be. HTTP recommends supporting at least 8000 octets; plenty of stacks still choke earlier.
  • Encoding is ugly and lossy-feeling. Nested JSON or binary-ish payloads in a query string is a tax you pay on every hop.
  • URIs get logged. Access logs, APM traces, browser history, and shared bookmarks all prefer the request line over the body. Sensitive filters (emails, account IDs, PII) leak more easily on GET than in a body.
  • Every parameter combo becomes a "resource." Stuffing the full query into the URI treats each unique filter set as a distinct resource identity, which muddies caching and linking semantics.

So teams reach for POST:

POST /feed HTTP/1.1
Host: example.org
Content-Type: application/x-www-form-urlencoded

q=foo&limit=10&sort=-published

Functionally fine. Semantically lying. POST is neither safe nor idempotent by default — intermediaries, browsers, and caches must assume it might change state. Automatic retries after a network blip become a judgment call. CDN caching of the response to that POST for future identical POSTs is not how HTTP caching is designed to work (POST responses are only cacheable for subsequent GET/HEAD in limited cases). You've invented "GET with a body" and labeled it wrong.

QUERY is the method that sentence was waiting for:

QUERY /feed HTTP/1.1
Host: example.org
Content-Type: application/x-www-form-urlencoded

q=foo&limit=10&sort=-published

Body like POST. Safety and idempotence like GET. Caches and retries can finally treat the request as what it is: a read.

What QUERY means, precisely

From RFC 10008: a QUERY asks the target resource to perform a query operation within the scope of that resource. The request content and its media type are the query. The server decides how far that scope reaches based on the resource you hit — QUERY /contacts is not the same as QUERY /orders, even with identical bodies.

Important properties:

Property QUERY
Safe Yes — client does not request or expect a change to the target resource
Idempotent Yes — safe to retry after connection failure
Request body Expected; semantics defined by the target resource + media type
Cacheable Yes — caches MAY reuse the response for later identical QUERY requests
Content-Type Required (and must match the body); missing/wrong → fail the request

A 200 OK means the query ran and the results are in the response body. That does not ban the server from creating additional resources (more on Location / Content-Location below) — it bans treating QUERY as a create/update/delete of the target itself.

Compare the three verbs side by side:

GET QUERY POST
Safe yes yes potentially no
Idempotent yes yes potentially no
Query lives in URI (by definition) request body request body
URI for the query itself yes optional (Location) no
URI for the result optional (Content-Location) optional (Content-Location) optional (Content-Location)
Cacheable as itself yes yes only for future GET/HEAD, with caveats
Body semantics "no defined semantics" expected expected

That middle column is the whole pitch.

A concrete example

Borrowing the spirit of the RFC's contacts example, here's a search that would be miserable as a GET query string and dishonest as a POST:

QUERY /contacts HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json

{
  "select": ["surname", "givenname", "email"],
  "limit": 10,
  "match": {
    "email": { "endsWith": "@example.com" }
  }
}
HTTP/1.1 200 OK
Content-Type: application/json

[
  { "surname": "Smith", "givenname": "John", "email": "[email protected]" },
  { "surname": "Jones", "givenname": "Sally", "email": "[email protected]" }
]

Same endpoint you already expose for GET /contacts. Different method, body-carried criteria, still a pure read.

Media types, errors, and Accept-Query

QUERY is media-type-aware on purpose. The body alone isn't enough — the Content-Type tells the server how to interpret the query. Servers must fail the request if Content-Type is missing or inconsistent with the content. No content sniffing to "fix" a bad header.

Practical status codes from the RFC:

  • 400 — no media type, or media type inconsistent with the body
  • 415 Unsupported Media Type — type known in principle, but this resource doesn't accept it for QUERY (advertise supported types via Accept-Query)
  • 422 Unprocessable Content — type understood, body well-formed for that type, but the query itself can't run (e.g. SQL referencing a missing table)
  • 406 Not Acceptable — client asked for a response media type the resource can't produce

Discovery of supported formats uses the new Accept-Query response header — a Structured Fields list of media ranges:

HEAD /contacts HTTP/1.1
Host: api.example.com
HTTP/1.1 200 OK
Accept-Query: application/json, application/sql;charset="UTF-8"

You can also discover method support the old-fashioned way:

OPTIONS /contacts HTTP/1.1
Host: api.example.com
HTTP/1.1 200 OK
Allow: GET, HEAD, OPTIONS, QUERY

Or just send a QUERY and handle 405 Method Not Allowed with an Allow header. Either works; documenting Accept-Query on your search resources is the polite move once you ship support.

Location, Content-Location, and turning QUERY into GET

RFC 10008 leans hard on a design principle of the web: important things get URIs. After a successful QUERY, a server may:

  • Send Content-Location pointing at a resource that represents these results — a later GET retrieves the same snapshot (possibly temporary).
  • Send Location pointing at an equivalent resource for the query itself — a later GET re-runs (or re-fetches) that query without resending the body.
HTTP/1.1 200 OK
Content-Type: application/json
Content-Location: /contacts/stored-results/17
Location: /contacts/stored-queries/42

[ ... results ... ]

That second header is operationally huge for caching. QUERY cache keys must incorporate the full request body and related metadata — harder than caching GET. If the server hands you a Location URI for the equivalent resource, clients (and CDNs) can switch to plain GET for follow-ups and get the simpler cache model back.

A 303 See Other redirect is another escape hatch: "don't QUERY again; GET this URI instead."

Caching and retries — why the verb matters

Because QUERY is safe and idempotent:

  • Clients and gateways can retry after a timeout without inventing application-level "was this a search or a create?" logic.
  • HTTP caches MAY store and reuse QUERY responses for subsequent identical QUERY requests (RFC 9111 rules apply; the cache key must include the body).
  • Caches may normalize insignificant body differences when building that key (strip content-encodings, JSON key order if the type allows it, etc.). Clients that care can send Cache-Control: no-transform.

This is the part POST /search never gave you for free. You could paper over it with custom headers and tribal knowledge. QUERY makes the contract visible on the wire.

Security and browser notes

A few footguns worth knowing before you flip the switch:

  • Prefer QUERY over GET when the filter set is sensitive. Bodies are less likely to land in access logs and Referer-shaped leakage than URIs — one of the explicit motivations in the RFC.
  • If you mint temporary Location / Content-Location URIs from a sensitive query, don't encode the secrets into those URIs.
  • CORS: QUERY is not a CORS-safelisted method. Browser cross-origin calls need a preflight, same as PUT/PATCH/DELETE. Plan for Access-Control-Allow-Methods to include QUERY.
  • Wrong cache normalization can false-positive two different queries as the same key. If your query language is subtle, be conservative about what intermediaries are allowed to rewrite.

When I'd use QUERY vs GET vs POST

My practical rules:

  1. Simple, short, non-sensitive filters → keep GET. It's still the best default: bookmarkable, cacheable everywhere, trivial to curl.
  2. Complex, large, or sensitive query criteria that are still pure readsQUERY. Search APIs, analytics filters, "give me products matching this JSON rule tree," SQL-ish report endpoints, anything you used to shamefully POST.
  3. Anything that creates, updates, enqueues, or otherwise changes server statePOST (or PUT/PATCH/DELETE). Don't launder mutations through QUERY just because the payload is JSON.

If you're designing a NestJS (or Express, Fastify, etc.) search surface today, the migration path is usually: keep GET /resources for list + simple filters, add QUERY /resources (or QUERY /resources/search if you prefer a dedicated target) for the heavy body, and stop documenting POST /search as a read. Framework and proxy support will trail the RFC for a while — custom method handling is already possible in most Node stacks, but CDNs, API gateways, and OpenAPI tooling need to catch up before QUERY is as boring as GET.

Wrapping up

QUERY doesn't replace GET or POST. It names the thing we were already doing with the wrong verb: a safe, idempotent request whose input is too rich for a URI. RFC 10008 (June 2026) makes that official — body-carried queries, cacheable responses, retry-friendly semantics, Accept-Query for discovery, and optional URIs for both the query and its results.

If your API still uses POST /search for reads, you now have a standards-backed answer for the next design review. Use GET when the query string is enough. Use QUERY when it isn't. Save POST for when the world actually changes.