Think about the last time you needed to search through a list of users, filter products by multiple criteria, or send a complex query to an API. You probably wrote a POST endpoint for it. Not because POST was the right choice — but because GET refused to carry a body, and jamming a JSON filter into a URL is a recipe for disaster.
For twenty years, that’s been the compromise. Pick the semantically wrong method (POST) or fight with URL length limits, encoding issues, and leaking sensitive data through logs (GET). Neither option is good. Both are workarounds for a gap that should never have existed.
In June 2026, the IETF closed that gap. RFC 10008 introduces the QUERY HTTP method — and it changes everything about how we design read-oriented APIs.
The Two Bad Options We’ve Been Stuck With
GET: The Clean Semantics That Can’t Handle Reality
GET is beautiful in theory. It’s safe, idempotent, cacheable, and any intermediary on the internet knows exactly what it means. The resource is fully identified by the URI, so caching just works and retries are always safe.
But GET has a fatal flaw: it can’t carry a body. RFC 9110, the core HTTP semantics spec, explicitly says content received in a GET request “has no generally defined semantics” and “might lead some implementations to reject the request.”
So everything has to go in the URL. That works fine when you’re filtering by a single field. It falls apart fast when you need:
- Nested filters with complex boolean logic
- Arrays of values
- Geo queries with coordinates and radius
- Full-text search with multiple options
The URL wasn’t designed for payloads. RFC 9110 only recommends servers support at least 8,000 octets — but that’s a floor, not a promise. Your local dev server might accept 16KB URLs. Your corporate proxy might cut off at 4KB. The 414 error you get is always from a system you didn’t test against.
There’s a subtler problem too: URLs leak. Every proxy, CDN, and server along the path logs the full URL. If your query contains an email, a user ID, or any sensitive data, it’s now sitting in log files across your infrastructure. The URL also ends up in browser history, bookmarks, and referrer headers.
And my personal favorite footgun: ?city=Berlin&limit=50 and ?limit=50&city=Berlin are semantically identical, but a naive cache treats them as different resources. That’s a surprisingly easy way to destroy your cache hit rate without realizing it.
POST: The Pragmatic Hack That Became Industry Standard
POST works today. That’s why every codebase has a POST /search or POST /query endpoint. You throw your JSON in the body, the server processes it, and you get results back. No URL size limits. No encoding headaches. It just works.
The cost is subtle but real. POST is not safe. It’s not idempotent. And for most caching layers, it’s not cacheable.
What does that mean in practice? When a POST connection drops mid-request, neither the client nor any intermediary knows whether the request was processed. Maybe it went through. Maybe it didn’t. You can’t safely retry without idempotency keys or deduplication logic — even though you were just reading data.
Caching is worse. RFC 9110 explicitly says “a POST request cannot be satisfied by a cached POST response.” Every dashboard that polls a search endpoint every 30 seconds is hitting your origin server on every single tick. Your users are paying for a full query execution — even when the exact same query was run 10 seconds ago.
POST is a lie. You’re telling the protocol “I might change state” when you’re only asking a question. And the protocol takes you at your word — no caching, no safe retries, no optimizations.
QUERY: The Method HTTP Should Have Always Had
RFC 10008 introduces QUERY as the third option. It’s exactly what the gap needed: a method that combines GET’s safety guarantees with POST’s ability to carry a body.
Feature GET QUERY POST
────────────────────────────────────────────
Safe ✅ ✅ ❌
Idempotent ✅ ✅ ❌
Cacheable ✅ ✅ ❌
Request body ❌ ✅ ✅
The key difference isn’t technical — it’s protocol-level. Because QUERY is registered in the IANA HTTP Method Registry as safe and idempotent, every generic HTTP client, proxy, and cache can treat it that way automatically. The guarantee moves from “please read our API docs” to “the protocol enforces this.”
The spec defines it simply: “A QUERY requests that the request target process the enclosed content in a safe and idempotent manner and then respond with the result of that processing.” In plain English: the request body is your question, and the response is the answer. No state changes. No side effects. Just a query and its results.
How QUERY Works in Practice
The Basics
A QUERY request is straightforward:
QUERY /contacts HTTP/1.1
Host: api.example.com
Content-Type: application/json
{
"filter": { "city": "Berlin" },
"select": ["name", "email"],
"limit": 50
}
The Content-Type header is mandatory. If you omit it or send something the server doesn’t understand, you get a 415 Unsupported Media Type.
Content Negotiation via Accept-Query
The spec introduces a new response header: Accept-Query. It lets servers advertise which query formats they accept:
HTTP/1.1 200 OK
Accept-Query: "application/jsonpath", application/sql;charset="UTF-8"
Clients can check this with OPTIONS or HEAD before sending a query. No more guessing and getting a 415.
The Equivalent Resource Idea
Here’s where QUERY shows its sophistication. The RFC introduces the concept of an equivalent resource — a conceptual GET-able resource that combines the target URI with the query content.
Servers can expose this through two headers that serve different purposes:
Content-Location— Points to a snapshot of the query results. “Here’s what this query returned at this moment.” Useful for caching a specific answer.Location— Points to the query itself as a reusable URI. “GET this URI to re-run the same query.” Turns an expensive QUERY into a cacheable GET on subsequent requests.
HTTP/1.1 200 OK
Content-Type: application/json
Content-Location: /contacts/queries/snapshot/a1b2c3d4
Location: /contacts/queries/saved/x8y9z0
[ ... results ... ]
Get the distinction wrong and your caching layer will serve stale data. Content-Location is a snapshot. Location is a reusable query.
Caching: The Hard Part
Caching a QUERY response is fundamentally harder than caching a GET. The cache key must include the full request body, not just the URI. Section 2.7 is explicit: caches MUST incorporate the request content into the key.
This means a cache has to read the entire body before it can check for a hit. To reduce misses from trivial differences, caches are allowed to normalize — stripping formatting differences, handling equivalent content encodings, and treating +json suffixes uniformly.
The danger: over-normalization can cause false-positive cache hits, returning wrong results for queries that happen to look similar after normalization. If you’re building a QUERY-aware cache, this is the bug to lose sleep over.
Error Handling
The RFC defines a clean error model:
| Status Code | Meaning |
|---|---|
400 Bad Request | Content-Type matched, but the body is malformed (invalid JSON, etc.) |
415 Unsupported Media Type | Content-Type missing or not supported |
422 Unprocessable Content | Valid format, but the query itself fails (references a non-existent field) |
405 Method Not Allowed | This resource doesn’t support QUERY |
The Long Road from SEARCH to QUERY
The idea for a query-carrying HTTP method first appeared in a 2015 draft from James Snell, originally called SEARCH. It stayed a draft for years, was revived at the 2019 HTTP Workshop, and got adopted by the httpbis working group in March 2021 — still named SEARCH.
The rename to QUERY happened in November 2021. The reason is buried in Appendix B of the RFC: WebDAV already defined PROPFIND, REPORT, and SEARCH — all safe, idempotent methods with bodies — and people have “mixed feelings” about WebDAV. QUERY got a clean slate.
The name also fits better. A URI has a “query component” (?key=value). QUERY extends that same idea to structured request bodies. It’s not a coincidence.
The timeline: 2015 draft → 2019 workshop revival → 2021 working group adoption → November 2025 final draft → November 2025 IESG approval → June 2026: RFC 10008 published. Eleven years from first draft to standard. HTTP doesn’t move fast, but when it does, it tends to stick.
Code: Making QUERY Work Today
Node.js (Native Support)
Node’s HTTP parser has supported QUERY since llhttp 9.2.0, which shipped in Node 21.7.2 and all Node 22+ releases. Zero configuration needed:
import { createServer } from 'node:http';
const server = createServer((req, res) => {
if (req.method !== 'QUERY') {
res.writeHead(405, { Allow: 'QUERY' });
return res.end();
}
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
const query = JSON.parse(body);
const results = searchContacts(query);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(results));
});
});
server.listen(3000);
Go (Arbitrary Method Strings)
Go doesn’t have a http.MethodQuery constant yet, but req.Method is just a string. Go 1.22’s ServeMux handles any method:
package main
import (
"encoding/json"
"io"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("QUERY /contacts", handleQuery)
mux.HandleFunc("GET /contacts/queries/{id}", handleStoredQuery)
http.ListenAndServe(":8080", mux)
}
func handleQuery(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
defer r.Body.Close()
var q struct {
Filter map[string]string `json:"filter"`
Limit int `json:"limit"`
}
json.Unmarshal(body, &q)
results := runQuery(q)
json.NewEncoder(w).Encode(results)
}
Client Side (fetch)
const res = await fetch('https://api.example.com/contacts', {
method: 'QUERY',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filter: { city: 'Berlin' },
limit: 50
})
});
const data = await res.json();
CORS note: QUERY is not a safelisted method. Every cross-origin request triggers an OPTIONS preflight. Plan for that extra round trip.
cURL
curl -X QUERY 'https://api.example.com/contacts' \
-H 'Content-Type: application/json' \
-d '{"filter": {"city": "Berlin"}, "limit": 50}'
Framework and Tooling Support (July 2026)
| Platform | What Works Today |
|---|---|
| Node.js 22+ | Native. Parser handles it out of the box. |
| Fastify | fastify.addHttpMethod('QUERY', { hasBody: true }) |
| Go 1.22+ | Works via string method names. No constant yet. |
| ASP.NET Core (.NET 11) | Preview 4 recognizes QUERY and emits it in OpenAPI |
| OpenAPI 3.2 | First-class query operation field |
| Spring Framework | PR #34993 open. Not merged. |
| Express | Works on Node 22+, but no TypeScript types |
| Browsers | fetch() accepts it. Always preflights cross-origin. |
The Spring situation is the one to watch. Issue #32975 sat dormant for two years. PR #34993 was marked ready for review the same week RFC 10008 published. As of July 2026, RequestMethod still doesn’t have a QUERY value, so you can’t use @RequestMapping with it. A servlet filter can intercept QUERY methods as a stopgap.
Should You Use QUERY Right Now?
Internal APIs: Yes
If you control both the client and server, there’s no reason to wait. Node 22 handles it natively. The semantics are standardized. Your internal proxies can be configured to support it. Every retry and cache you control starts benefiting immediately.
Public APIs: Adopt Alongside POST
Ship Accept-Query on endpoints that support it. Keep your POST search endpoint for older clients. Document both in OpenAPI 3.2. Let early adopters use QUERY while everyone else stays on the legacy path. When the tooling catches up — generated clients from OpenAPI specs, CDN support, browser maturity — you can deprecate the POST endpoint.
The Rollout Strategy
- Implement QUERY on your search/query endpoints
- Start returning
Accept-Queryheaders - Let internal services migrate first
- Document both methods in your API specs
- Deprecate POST search in 6-12 months
The Bottom Line
POST /search has been the industry’s pragmatic lie for two decades. We all knew it was semantically wrong. We all used it anyway because the alternative — cramming complex queries into URLs — was worse.
RFC 10008 finally fixes this. QUERY is safe where POST isn’t. It carries a body where GET can’t. It’s cacheable, idempotent, and protocol-level guaranteed. The method finally matches the meaning.
The fact that it took 11 years and an IETF standard to go from draft to publication tells you how seriously the HTTP community takes method changes. This isn’t a throwaway experiment. It’s a carefully designed, thoroughly reviewed addition to the protocol that will be with us for decades.
POST /search was always a hack. We just didn’t have a choice — until now.
Try this today: Spin up a Node 22 server with a single QUERY handler. curl it from your terminal. The moment you realize you can retry without fear and cache without hacks is the moment
POSTfor reads starts feeling like what it always was: a workaround.
References
- RFC 10008: The HTTP QUERY Method
- RFC 9110: HTTP Semantics
- OpenAPI 3.2.0 Release
- Spring PR #34993
- IANA HTTP Method Registry
Happy querying 🚀
Member discussion
0 commentsStart the conversation
Become a member of >hacksubset_ to start commenting.
Already a member? Sign in