The HTTP QUERY method ends two decades of POST abuse
RFC 10008 standardizes a safe, idempotent, cacheable request with a body, but real-world support is still years out.
Every API developer has hit this wall. You need to send a complex query to the server: nested filters, an array of IDs, a chunk of structured criteria that won't fit cleanly in a URL. GET is the semantically correct method, since you're reading, not mutating. But GET has no business carrying a body, and stuffing everything into query parameters either blows past URL limits or turns into the ?roles[]=admin&roles[]=reporter versus ?roles=admin&roles=reporter guessing game. So you reach for POST, and the moment you do, you've lied to the entire HTTP stack about what your request does.
That lie has consequences. Caches won't touch it. Retry logic gets nervous. Intermediaries treat a read as a state change. The new QUERY method, published as RFC 10008 in June 2026, finally closes the gap. My read: it's the correct fix to a genuinely annoying problem, and the spec is clever in ways that go beyond "GET but with a body." It's also years away from being something you can put on a public API without a fallback. Both things are true.
A workaround that calcified into a convention
The pain here is old. The IETF draft that became RFC 10008, draft-ietf-httpbis-safe-method-w-body, traces back years (a 2021 version already named the method QUERY), and the underlying idea goes back further to WebDAV's SEARCH. The very filename, safe-method-with-body, tells you the whole story: HTTP never had a safe, idempotent method that was allowed to carry content.
People tried both bad options. Sending a body with GET isn't explicitly forbidden by the HTTP RFCs, but they strongly advise against it, and the result is exactly the interop mess you'd expect. As Kreya notes, some clients and proxies reject a GET body outright, some silently drop it, some honor it. Elasticsearch famously leaned on GET-with-body for its _search endpoint and had to support a POST alias precisely because so much middleware mangles it.
So POST became the de facto query method. GraphQL runs its reads over POST. JSON-RPC, by design, ships a JSON envelope describing the method and params, which means a body, which means POST, which means every semantically-read call gets treated as a write. You lose HTTP-native caching and end up building a bespoke application-level cache to compensate. That's the road RFC 10008 is trying to get you off.
What the spec actually guarantees
QUERY is deliberately boring, and that's the point. It's GET's semantics (safe and idempotent) with POST's ability to carry a request body. The cleanest way to see where it sits is the property table from the spec:
| Property | GET | QUERY | POST |
|---|---|---|---|
| Safe | yes | yes | potentially no |
| Idempotent | yes | yes | potentially no |
| Cacheable | yes | yes | yes, but only for future GET/HEAD |
| Request body | no defined semantics | expected | expected |
The caching line is where it earns its keep. A QUERY response is cacheable, but the cache key has to incorporate the request body and its metadata, not just the URL. That single rule is what lets a CDN or reverse proxy look at the Content-Type plus the body bytes and serve a hit for an identical query. The spec even permits caches to normalize the body (reorder JSON keys, strip insignificant whitespace) to lift hit rates. Compare that to POST, whose responses are technically cacheable but only ever for a later GET or HEAD, which is useless for the query-in-the-body case.
The subtler bit of design is the URI dance. The spec lets a server hand back a Location header pointing at the query itself, and a Content-Location pointing at a specific result, both usable in a later GET. So you can send a fat QUERY body, get back a canonical, bookmarkable URL, and reads of that URL go through the normal GET path. That neatly answers the "any important resource ought to have a URI" principle without forcing every query permutation into the URL up front. An empty result set comes back as a plain 204 No Content.
Note the bus factor on authorship has shifted toward the people who actually run the pipes: the current draft is credited to Julian Reschke (greenbytes), James Snell now at Cloudflare, and Mike Bishop at Akamai. When two of the three largest CDN operators are co-authoring the method, the caching story isn't an afterthought.
How you'd actually adopt it
The good news: clients need almost nothing. HTTP methods are just strings, so most mature HTTP libraries already let you send QUERY today. In Go's net/http you pass it straight to http.NewRequestWithContext:
req, _ := http.NewRequestWithContext(ctx, "QUERY", "https://rpc.example.com", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
Rust's reqwest is the same idea via Method::from_bytes(b"QUERY"). No new dependency, no fork. On the tooling side, Kreya shipped first-class QUERY support in its 1.20 release, which is a useful signal that API clients are starting to treat it as a real method rather than a custom string.
Where it's genuinely worth designing for now:
- JSON-RPC and GraphQL gateways, where every call is structurally a
POSTbut semantically a read. Moving reads toQUERYrestores idempotency for retries and opens the door to edge caching you currently fake in the app layer. - Search and reporting endpoints with deep filter objects that don't fit a query string.
- Internal service-to-service APIs behind infrastructure you control end to end, which is exactly where you can guarantee the whole path speaks
QUERY.
The caching gotcha is real and worth planning for: keying on the body means your cache layer has to read and ideally normalize request content. That's more work than fingerprinting a URL, and a naive implementation that keys on raw bytes will miss hits on semantically-identical-but-differently-formatted JSON. Lean on the normalization the spec allows.
The catch nobody should skip
Don't go rewriting your public GET endpoints this quarter. The weak link isn't your code, it's everything between your client and your server. Corporate firewalls, legacy proxies, WAFs and older web servers tend to allow only the well-known methods and will reject anything unfamiliar, just as they did when PATCH was new. Until that long tail catches up, a QUERY request from a user behind a strict proxy may simply fail.
Two more constraints, by design:
- It isn't shareable. A query that lives in a request body can't be pasted into a browser address bar or saved as a bookmark. If users need to share filtered views, plain
GETwith URL params stays the right tool (or use thatLocationround-trip to mint aGET-able URL). - Browser forms still only emit
GETandPOST.fetchcan sendQUERY, but the classic form path can't, so progressive-enhancement scenarios need a fallback.
The honest read: RFC 10008 fixes a design flaw that's been quietly costing developers cache hits and clean retry logic for twenty years, and the spec is well-considered, particularly the body-aware cache key and the Location escape hatch. But a method is only as deployable as the least capable box on the path, and that box is going to be ancient for a while. Build it into your SDKs and internal services now, test aggressively, and keep a POST fallback wired up for the public internet. Treat QUERY as the method you migrate toward, not the one you bet a production launch on this year.
Sources & further reading
- The new HTTP QUERY method explained — kreya.app
- RFC 10008: The HTTP QUERY Method — blainsmith.com
- The HTTP QUERY Method — httpwg.org
- The HTTP QUERY Method — ietf.org
Lenn writes about cloud platforms, Kubernetes internals, and the infrastructure decisions that quietly make or break engineering organizations. Based in Berlin's vibrant tech scene, they have a talent for turning dense platform-engineering topics into prose that people actually finish reading.
Discussion 1
i've lost count of how many times i've had to explain to someone why we can't just use post for everything, so it's great to see a standardized alternative emerging with the http query method - now if only we could get yaml to behave itself in our k8s configs 🙃