July 3, 2026
· 11 min readThe Missing HTTP Method: QUERY Is Almost Here
Complex queries don't fit in GET URLs, so we abuse POST and lose caching, safety, and automatic retries. The HTTP QUERY method — now in the RFC Editor queue as a Proposed Standard — fixes this. This deep dive covers the spec's semantics, caching rules, the Accept-Query header, and practical implementation patterns for Laravel and Node.js.

You've done this. Everyone has done this:
POST /api/products/search HTTP/1.1
Content-Type: application/json
{ "filters": { "category": "electronics", "price": { "lt": 500 } } }That request reads data. It changes nothing. Yet it's a POST — which means no shared caching, no safe automatic retries, and every intermediary between your client and server treats it as a state-changing write. For 25 years, HTTP has forced this lie on us because neither GET nor POST fits a complex read.
That gap is finally closing. The HTTP QUERY method has cleared IESG evaluation and is sitting in the RFC Editor's queue as a Proposed Standard.
TL;DR
- QUERY is a new HTTP method: safe and idempotent like GET, but with a request body like POST.
- Spec:
draft-ietf-httpbis-safe-method-w-body(Reschke, Snell, Bishop) — in the RFC Ed Queue as of mid-2026; RFC number not yet assigned. - Responses are cacheable, but the cache key must include the request content, not just the URL.
- Servers must reject QUERY requests without a valid
Content-Type— no content sniffing. - A new
Accept-Queryresponse header advertises which query media types a resource accepts. - Not CORS-safelisted: cross-origin QUERY always triggers a preflight.
- You can route QUERY in Laravel and Node.js today with minor workarounds — the real adoption blocker is intermediary (proxy/CDN) support.
Why QUERY matters
HTTP has two verbs for "give me data," and both are broken for complex queries.
GET puts everything in the URL. That works until your query is a 40-line Elasticsearch DSL document or a GraphQL operation. URLs hit practical length limits across uncoordinated systems (servers, proxies, logs, browsers), URI-encoding a structured query is painful, and URLs leak into access logs, browser history, and referrer headers — a real problem when the query contains sensitive filters.
Important: RFC 9110 doesn't forbid a GET body — it says the body has no defined semantics. Some servers read it, some drop it, some reject the request. Elasticsearch famously accepts
GET _searchwith a body, and famously has to document POST as the fallback because so many clients and proxies mangle it.
POST carries the body fine but throws away the semantics. A POST is, by definition, neither safe nor idempotent. Consequences:
- ❌ Shared caches won't cache it generically
- ❌ Clients and proxies can't retry it automatically after a connection drop — was the "write" applied?
- ❌ Crawlers, prefetchers, and tooling must assume it mutates state
- ❌ Idempotency has to be bolted on with
Idempotency-Key-style headers
So every search API, GraphQL endpoint, and reporting service pretends reads are writes. QUERY spans exactly this gap between GET and POST: the request target processes the enclosed content in a safe, idempotent manner and responds with the result — similar to POST, but automatically repeatable without concern for partial state changes.
How QUERY works
A QUERY request looks like a POST but means something entirely different:
QUERY /contacts HTTP/1.1
Host: example.org
Content-Type: example/query
Accept: text/csv
select surname, givenname, email limit 10HTTP/1.1 200 OK
Content-Type: text/csv
surname, givenname, email
Smith, John, john.smith@example.org
Jones, Sally, sally.jones@example.comBreaking it down:
- The body is the query — SQL-like text, JSON, JSONPath, GraphQL, whatever media type the resource supports.
Content-Typeis mandatory. The spec requires servers to reject requests where it's missing or inconsistent with the actual content — content sniffing is explicitly off the table. This closes a class of request-smuggling and misinterpretation bugs.- The response is the query result, with normal representation headers.
Safe and idempotent — what that buys you
"Safe" means the client isn't requesting any state change; "idempotent" means N identical requests have the same effect as one. Because QUERY guarantees both:
- 🚀 A client or proxy can automatically retry a QUERY that died mid-flight — impossible to do safely with POST.
- ⚡ Intermediaries can cache results (more below).
- 🔒 Security tooling and crawlers can treat QUERY endpoints as read-only surfaces.
The equivalent-resource trick: Location and Content-Location
This is the spec's most underrated feature. A server responding to QUERY can include:
Location— a URI where the same query can be re-run later via plain GET. The server essentially mints a bookmarkable URL for your body-based query.Content-Location— a URI where the current result is stored as a retrievable resource.
Your API can accept rich body queries and hand back cheap, cacheable GET URLs for repeat consumers.
Accept-Query: discoverable query languages
A resource advertises which query formats it understands via the new Accept-Query response header (a Structured Field of media types):
HTTP/1.1 200 OK
Accept-Query: "application/jsonpath", "application/sql"Send a QUERY with an unsupported format and the server responds 415 Unsupported Media Type — and can include Accept-Query to tell you what would work. That's protocol-level content negotiation for query languages, something GraphQL/OData/Elasticsearch each reinvented privately.
Conditional queries
Because QUERY responses are ordinary representations, conditional request machinery works. Send If-None-Match with the ETag from a previous result, and if the query result hasn't changed, the server returns 304 Not Modified. Polling dashboards just got dramatically cheaper.
Caching: the hard part done right
GET caching keys on the URL. QUERY can't — two requests to the same URL with different bodies are different queries. The spec's answer:
The cache key must incorporate the request content (and relevant metadata like
Content-Type), not just the method and target URI.
In practice, caches will hash the body into the key. The spec also permits a clever optimization: caches may normalize semantically insignificant differences in request content — for example, removing content encoding differences — to improve hit rates. Two clients sending byte-different but semantically identical queries can share a cache entry if the cache understands the query media type.
| Concern | GET | POST | QUERY |
|---|---|---|---|
| Request body | ❌ Undefined semantics | ✅ Yes | ✅ Yes, defined |
| Safe | ✅ | ❌ | ✅ |
| Idempotent | ✅ | ❌ | ✅ |
| Cacheable | ✅ (URL key) | ⚠️ Only with explicit freshness, rarely done | ✅ (URL + body key) |
| Auto-retry safe | ✅ | ❌ | ✅ |
| Query size limit | ⚠️ URL length | ✅ None practical | ✅ None practical |
| Sensitive data in URL/logs | ❌ Leaks | ✅ In body | ✅ In body |
| CORS preflight | Often none | Often none (simple requests) | ⚠️ Always |
⚠️ Warning: QUERY bodies keep sensitive filters out of URLs and access logs — but they're still request content. Your application logging and cache layers now hold query payloads; treat them with the same care you'd give POST bodies.
Implementing QUERY today
Here's the honest state of the world: the spec is done, but the plumbing — CDNs, proxies, framework sugar — is still rolling out. The good news: HTTP methods are just tokens, and most stacks pass unknown methods through if you ask.
Laravel / PHP
Laravel has no Route::query() helper yet, but Route::match() accepts arbitrary verbs:
// routes/api.php
use Illuminate\Support\Facades\Route;
Route::match(['QUERY'], '/products', [ProductQueryController::class, 'handle']);// app/Http/Controllers/ProductQueryController.php
class ProductQueryController extends Controller
{
public function handle(Request $request)
{
// Spec: reject missing/mismatched Content-Type — no sniffing
if ($request->header('Content-Type') !== 'application/json') {
return response()->json(
['error' => 'Unsupported query media type'],
415,
['Accept-Query' => '"application/json"']
);
}
$query = $request->json()->all();
$results = Product::filter($query)->get();
// Cache key = URL + hash of the query body
$etag = '"' . hash('xxh128', $request->getContent()) . '-' . $results->max('updated_at')?->timestamp . '"';
if ($request->header('If-None-Match') === $etag) {
return response()->noContent(304);
}
return response()->json($results)
->header('ETag', $etag)
->header('Cache-Control', 'private, max-age=60');
}
}Breaking it down:
Route::match(['QUERY'], ...)— Laravel's router matches any method string; no core changes needed.- The 415 +
Accept-Queryresponse follows the spec's negotiation pattern. - The ETag incorporates the request body hash, mirroring the spec's cache-key rule at the application layer.
💡 Tip: If you run behind nginx, confirm it forwards QUERY. nginx proxies unknown methods by default, but
limit_exceptblocks and some WAF rules whitelist methods — audit those before shipping.
Node.js
fetch (and undici under the hood) sends custom methods without complaint:
// Client
const res = await fetch('https://api.example.org/products', {
method: 'QUERY',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ category: 'electronics', price: { lt: 500 } }),
});
const products = await res.json();On the server, Express routes custom methods via router.all() with a method guard, or you can drop to the raw http server which accepts any registered token. Fastify needs the method added to its accept list:
// Fastify
const fastify = require('fastify')();
fastify.addHttpMethod('QUERY', { hasBody: true });
fastify.route({
method: 'QUERY',
url: '/products',
handler: async (req, reply) => {
const results = await searchProducts(req.body);
return reply
.header('Cache-Control', 'private, max-age=60')
.send(results);
},
});Fallback strategy for the transition years
Until your entire path (client → CDN → LB → app) speaks QUERY, run a dual-endpoint pattern:
Point both routes at one handler. Advertise the QUERY route in your API docs as preferred; keep the POST alias until your traffic analytics say intermediaries have caught up.
Where this came from (and where it's going)
The idea is old — WebDAV shipped a SEARCH method back in RFC 5323 (2008), but it dragged WebDAV-specific baggage and never generalized. The effort was revived after discussion at the 2019 HTTP Workshop, and the working group ultimately chose the name QUERY — partly because it maps cleanly to the URI's query component it's replacing.
The draft is authored by Julian Reschke (greenbytes), James M. Snell (Cloudflare), and Mike Bishop (Akamai) — note the CDN heavyweights on that author list; the people who run the world's caches want this. It went through a notably rigorous late review cycle: Roy Fielding (author of HTTP's core semantics and the REST dissertation) marked an earlier revision "Not ready," and revisions 12 through 14 addressed that feedback before the document cleared the IESG.
As of July 2026 the draft sits in the RFC Editor queue — final editorial processing before a number is assigned and it becomes a Proposed Standard. If you've seen a specific RFC number floating around for it (I've seen "RFC 10008" claimed), be skeptical: no number has been assigned, and the RFC series hasn't reached the 10000s.
Watch for adoption signals from Elasticsearch (whose GET _search-with-body is the poster child for this problem), GraphQL-over-HTTP, and the major CDNs.
Production checklist
- Enforce
Content-Type— reject QUERY requests with missing or mismatched types with 415; never sniff. - Advertise
Accept-Queryon your queryable resources so clients can discover supported formats. - Key caches on the body — URL + content hash (+
Content-Type), never URL alone. - Keep handlers genuinely side-effect free — QUERY promises safety; logging is fine, mutations are not.
- Emit ETags and honor conditional requests — 304s on unchanged query results are QUERY's cheapest win.
- Audit intermediaries — WAF method whitelists,
limit_exceptblocks, and CDN configs that drop unknown methods. - Plan for CORS preflights — QUERY is never a "simple request"; make sure
Access-Control-Allow-Methodsincludes it. - Ship a POST fallback route to the same handler during the transition.
When to use / when not to
Use QUERY when:
- Query parameters are too large or structured for a URL (search DSLs, GraphQL, report filters)
- The query contains sensitive values you don't want in URLs and logs
- You want cacheable, retryable reads with a body
Stick with GET when:
- The query fits comfortably in a short query string — GET remains simpler and universally cached
- You need zero-preflight cross-origin requests
Stick with POST when:
- The operation actually mutates state (obviously)
- The "query" has side effects — kicking off an async report job that stores results is a POST, not a QUERY
Conclusion
I've lost count of the read-only POST endpoints I've shipped over the years — every search API, every report filter, every GraphQL gateway. QUERY is the method those endpoints always wanted to be, and with the spec now in the RFC Editor's queue and Cloudflare and Akamai engineers as co-authors, the infrastructure will follow. Start now: route QUERY to your existing search handlers behind a POST fallback, enforce Content-Type strictly, and key your caches on the body. When the RFC number lands, you'll already be compliant.
FAQ
What is the HTTP QUERY method?
QUERY is a new HTTP method defined by the IETF HTTP Working Group. It is safe and idempotent like GET, but carries a request body like POST — designed for complex queries that don't fit in a URL.
Is QUERY an official RFC yet?
Not quite. As of mid-2026, draft-ietf-httpbis-safe-method-w-body has passed IESG evaluation and sits in the RFC Editor's publication queue as a Proposed Standard. An RFC number has not been assigned yet.
Why not just send a body with GET?
RFC 9110 says a GET body has no defined semantics. Proxies, CDNs, servers, and caches handle GET bodies inconsistently — many drop them silently. QUERY gives body-carrying reads well-defined, interoperable semantics.
How is QUERY different from POST?
POST is neither safe nor idempotent, so clients and intermediaries can't retry it automatically or cache its responses generically. QUERY is both safe and idempotent, so failed requests can be repeated without side-effect concerns and responses are cacheable.
Are QUERY responses cacheable?
Yes — but the cache key must incorporate the request content, not just the URL. Caches may normalize semantically insignificant differences in the body to improve hit rates.
Do browsers support QUERY?
fetch() can send arbitrary methods, but QUERY is not a CORS-safelisted method, so cross-origin QUERY requests always trigger a preflight. Framework and infrastructure support is still rolling out.
Can I use QUERY in Laravel or Express today?
Yes, with small workarounds. Laravel's Route::match() and Express's router.all() (or app.query() once frameworks add first-class helpers) can route QUERY requests. The bigger consideration is your proxy/CDN layer passing the method through.
What happened to the SEARCH method?
SEARCH exists in WebDAV (RFC 5323) but carries WebDAV-specific baggage. The working group renamed the generic effort to QUERY, partly because it maps cleanly to the URI's query component.