How a brand-new, safe-but-body-carrying HTTP method finally fixes the awkward POST /search hack every backend developer has written at least once.
Introduction
Quick question: how many times have you built a POST /search endpoint that doesn’t actually change anything?
If you’ve been writing backends for more than a year, the honest answer is probably “more than I’d like to admit.” You needed to send a complicated filter object to the server, GET couldn’t carry it, so you reached for POST — even though the operation was purely read-only. It worked. It also quietly lied to every cache, proxy, and monitoring tool in the request path about what your endpoint really does.
HTTP methods matter because they are promises. GET promises “I’m just reading.” DELETE promises “something is going away.” Caches, CDNs, load balancers, retry logic, and security tooling all make decisions based on these promises. When you use the wrong method, you break the contract — and the whole ecosystem around your API starts guessing.
In June 2026, the IETF published RFC 10008: The HTTP QUERY Method. It’s an Internet Standards Track document — the same standing as HTTP/1.1 itself — and it defines a genuinely new HTTP method called QUERY. It’s the first new method to be standardized since PATCH back in 2010. QUERY is designed for exactly the situation above: a safe, idempotent, cacheable request that carries its input in the request body instead of cramming it into the URL.
So why haven’t you heard much about it? Because it’s new. The standard is settled, but the tools around it — frameworks, servlet containers, browsers, gateways — are still catching up. That gap between “the spec exists” and “my framework has a @QueryMapping” is exactly what this article is about. We’ll cover what QUERY actually is, what the RFC really says, and — honestly — what you can and can’t use today.
Quick Refresher: The HTTP Methods You Already Know
Before the new arrival, let’s make sure we’re all on the same page about the classics. Think of HTTP methods as verbs — they describe the action you want to take on a resource (a “noun” identified by a URL).
- GET — “Give me a copy of this.” Retrieval only. It’s safe (it shouldn’t change anything) and idempotent (calling it once or a hundred times has the same effect). Because of this,
GETresponses are the bread and butter of HTTP caching. - POST — “Here’s some data; do something with it.” The general-purpose workhorse. Typically used to create resources or trigger processing. It’s neither guaranteed safe nor idempotent — send it twice and you might create two orders.
- PUT — “Store this representation at this exact URL.” Used to create or fully replace a resource at a known location. It’s idempotent:
PUTthe same thing twice and you end up in the same state. - PATCH — “Apply this partial update.” Like
PUT, but you send only the fields that changed rather than the whole resource. Not guaranteed idempotent. - DELETE — “Remove this resource.” Idempotent — deleting something that’s already gone still leaves it gone.
Here’s the quick mental model: use GET to read, POST to create or run actions, PUT to replace, PATCH to tweak, and DELETE to remove. Simple enough — until you need to read something using data too big to fit in a URL. That’s where the cracks appear.
The Problem Before QUERY
GET is the natural choice for reading data. For years, we expressed searches as query strings:
GET /products?category=laptop&brand=dell&minPrice=500&maxPrice=1500&inStock=true
That’s fine for a handful of parameters. But modern search and filtering endpoints aren’t simple anymore. Users want faceted filters, nested boolean logic, ranges, sort orders, geo boxes, and pagination — sometimes across dozens of fields. Try to encode that into a URL and you run into a wall of problems:
- URL length limits. RFC 9110 recommends that senders and recipients support request targets of at least 8,000 octets, but that’s a floor, not a guarantee. A request can pass through many uncoordinated systems — browsers, proxies, gateways, WAFs — and any one of them may impose a tighter, undocumented limit. You often don’t find out until something silently truncates or rejects your request in production.
- Complex, nested filters. Query strings are flat key-value pairs. Expressing a nested structure like
filters.price.between = [500, 1500] AND (brand IN [dell, lenovo])turns into an unreadable, bracket-and-encode nightmare. - Sensitive data in URLs. URLs get logged — in access logs, proxy logs, browser history, and bookmarks — far more readily than request bodies do. Putting a customer identifier or an auth-ish token in a query string is a genuine privacy and security concern. The RFC explicitly calls this out as a reason to prefer keeping query data in the body.
- Difficult encoding. Every special character has to be percent-encoded. Complex queries become long, brittle, error-prone strings that are painful to build and debug.
So developers did the pragmatic thing: they moved the query into a request body and used POST.
POST /products/searchContent-Type: application/json{ "category": "laptop", "brand": ["dell", "lenovo"], "price": { "min": 500, "max": 1500 }, "inStock": true}
This solves the encoding and size problems beautifully. But it introduces a semantic problem: POST is not safe and not idempotent. Nothing in the protocol signals that this particular POST is actually a harmless, repeatable read. So:
- Caches won’t reuse the response the way they would for a read, because
POSTresponses generally aren’t cacheable for subsequent requests to the same URL. - Automatic retries become risky. A generic retry layer can’t safely re-send a
POSTafter a timeout — for all it knows, it might duplicate a side effect. - Observability lies. Your dashboards count these as writes. Anyone reading the API sees
POST /searchand has to know, out of band, that it’s read-only.
You’ve been using a “write” verb to describe a “read.” It works, but it’s semantically wrong — and everyone downstream pays a small tax for the ambiguity.
Introducing QUERY
QUERY is the method that closes this gap. It takes the body support from POST and the safe, idempotent semantics from GET.
Here’s the same search expressed with QUERY:
QUERY /products HTTP/1.1Host: api.example.comContent-Type: application/jsonAccept: application/json{ "category": "laptop", "brand": ["dell", "lenovo"], "price": { "min": 500, "max": 1500 }, "inStock": true}
Notice it targets /products directly — not /products/search. There’s no need for a “verb noun” like /search in the path anymore, because the method already says “this is a query.”
Let’s unpack the concepts the RFC defines:
- What QUERY is. A method to ask a resource to perform a query described by the request content, and return the result. The content and its media type define the query; the target URI scopes what’s being queried.
- A safe method. The client isn’t requesting or expecting any change to the server’s state. Like
GET, it’s a pure read as far as the target resource is concerned. (The server may create helper resources to hold results — more on that below — but that’s not a change the client asked for.) - Idempotent. Repeating a
QUERYproduces the same effect as sending it once, so it can be safely retried after a dropped connection. - Request body support. The query input lives in the request content, not the URL. Crucially, the RFC says the server MUST fail the request if the
Content-Typeis missing or inconsistent with the body — the media type is mandatory, because it’s what gives the body meaning. - Cacheable — with a twist.
QUERYresponses can be cached. But the cache key MUST incorporate the request body (and related metadata), not just the URL. This is the single most important operational detail to internalize, because a cache that ignores the body would happily serve the wrong results. - Content negotiation.
QUERYsupports the usual machinery:Acceptto ask for a response format, plus a dedicatedAccept-Queryresponse header so a resource can advertise which query formats it accepts (for exampleapplication/sql,application/jsonpath, orapplication/x-www-form-urlencoded).
The RFC even provides ways to bridge back to GET. A successful QUERY response can include:
Content-Location— a URL where the client canGETthese specific results again.Location— a URL where the client canGETto re-run the same query later without resending the body.

This is genuinely elegant: you get body-carried queries and a path back into the plain, cache-friendly, bookmarkable GET world.
Practical Examples
Let’s walk through the same conceptual “find products” operation three ways.
Example 1 — Simple GET
Perfect when the filter is small and non-sensitive:
GET /products?category=laptop&inStock=true HTTP/1.1Host: api.example.comAccept: application/json
Nothing wrong here. If your filters genuinely fit in a URL, GET is still the right answer — the RFC itself notes that for short queries you’re better off with GET.
Example 2 — POST Search
POST /products/search HTTP/1.1Host: api.example.comContent-Type: application/json{ "category": "laptop", "brand": ["dell", "lenovo"], "price": { "min": 500, "max": 1500 }, "specs": { "ram": ">=16GB", "storage": "SSD" }, "sort": [{ "field": "price", "order": "asc" }], "page": 1, "pageSize": 20}
This handles the complex payload, but it’s a read masquerading as a write. Caches skip it, retries are risky, and the /search suffix is a smell that the method couldn’t express intent on its own.
Example 3 — QUERY Request
QUERY /products HTTP/1.1Host: api.example.comContent-Type: application/jsonAccept: application/json{ "category": "laptop", "brand": ["dell", "lenovo"], "price": { "min": 500, "max": 1500 }, "specs": { "ram": ">=16GB", "storage": "SSD" }, "sort": [{ "field": "price", "order": "asc" }], "page": 1, "pageSize": 20}
Same payload, but now the semantics are honest. Infrastructure that understands QUERY knows this is safe, idempotent, and cacheable, and it can act accordingly. The endpoint is just /products — the method carries the meaning.
Java HttpClient Example
Good news: you can send a QUERY request with Java’s built-in HttpClient today, no libraries required. Here’s a complete example using Java 21:
import java.net.URI;import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpRequest.BodyPublishers;import java.net.http.HttpResponse;import java.net.http.HttpResponse.BodyHandlers;public class QueryExample { public static void main(String[] args) throws Exception { HttpClient client = HttpClient.newHttpClient(); String queryBody = """ { "category": "laptop", "brand": ["dell", "lenovo"], "price": { "min": 500, "max": 1500 }, "page": 1, "pageSize": 20 } """; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.example.com/products")) .header("Content-Type", "application/json") .header("Accept", "application/json") // No dedicated .QUERY() builder — use the generic method(...) .method("QUERY", BodyPublishers.ofString(queryBody)) .build(); HttpResponse<String> response = client.send(request, BodyHandlers.ofString()); System.out.println("Status: " + response.statusCode()); System.out.println("Body: " + response.body()); }}
Why is there no dedicated .QUERY() method?
HttpRequest.Builder ships convenience methods only for the long-established verbs: .GET(), .POST(...), .PUT(...), and .DELETE(). That API was designed back in Java 11 (2018), years before QUERY existed.
But the builder also exposes a generic escape hatch:
public HttpRequest.Builder method(String method, BodyPublisher bodyPublisher)
This accepts any valid HTTP method token as a string. So QUERY — and any future method — works without waiting for a new JDK release to add a helper. There’s simply no need for a .QUERY() shortcut when .method("QUERY", ...) already does the job cleanly. (The convenience methods are just thin wrappers over method(...) anyway.)
Advantages
Why bother, once the ecosystem is ready?
- Better semantics. Reads are finally declared as reads. Caches, proxies, retry layers, and dashboards can all trust the method again.
POST /searchstops being a lie. - Large request payloads. Complex, deeply-nested filter objects live comfortably in the body — no URL length anxiety, no percent-encoding gymnastics.
- Cleaner APIs. Endpoints become nouns again.
QUERY /productsinstead ofPOST /products/search. Your URL structure describes resources, and the method describes the action. - Better than POST for searching. You keep the payload flexibility of
POSTwhile regaining the safety, idempotency, and cacheability ofGET— including automatic retries and a defined path back toGETviaLocation/Content-Location. - Privacy. Sensitive filter values stay out of URLs, logs, and browser history.
Limitations
Being fair about the downsides:
- Limited ecosystem support. This is the big one. A perfect method that half your stack doesn’t recognize is a liability, not an asset.
- Tooling gaps. Not every API client, documentation generator, code generator, or testing tool understands
QUERYyet. Expect rough edges. - Firewalls and WAFs. Security appliances with method allowlists may reject
QUERYoutright, or — worse — inspect and route it inconsistently across layers. - Infrastructure compatibility. CDNs and caches that don’t incorporate the request body into the cache key can return wrong results or open the door to cache poisoning. Caching a body-keyed method correctly is genuinely harder than caching
GET. - Framework support. As covered, Spring, ASP.NET, and others are mid-migration. You may be writing bridge code for a while.
Conclusion
For years we’ve had two imperfect options for complex reads: GET, which is semantically correct but can’t carry a body, and POST, which carries a body but lies about being safe. QUERY, standardized as RFC 10008 in June 2026, is the missing third option — safe and idempotent like GET, body-carrying like POST, cacheable when done right, and honest about its intent.
The catch is timing. The standard is finished; the ecosystem isn’t. Java’s HttpClient can send QUERY today, but Spring’s native support is still an open PR targeting version 7.1, browsers need preflights, and plenty of infrastructure predates the method. That makes QUERY an excellent choice for internal, controlled APIs right now — and a “watch this space, keep a fallback” choice for public ones.
Learn it now. Experiment with it on backend-to-backend calls. When your framework ships support, you’ll already know exactly where it belongs.
Key Takeaways
QUERYis a real, standardized HTTP method — RFC 10008, IETF Standards Track, published June 2026. It’s the first new method sincePATCHin 2010.- It combines the best of both worlds: safe and idempotent like
GET, body-carrying likePOST, and cacheable — the cache key must include the request body. - It fixes the
POST /searchanti-pattern, giving read-only operations honest semantics that caches, proxies, retries, and monitoring can trust. Content-Typeis mandatory, and responses can hand youLocation/Content-LocationURLs to bridge back to plainGET.- Java
HttpClientsupports it today via.method("QUERY", ...); there’s no dedicated builder because the generic one already handles any method. - Spring has no native support yet — routing goes through a
RequestMethodenum with noQUERYconstant. Native support is an open PR targeting Spring Framework 7.1. Use a servletFilteras a bridge for now. - Ecosystem support is real but uneven: Node.js and OpenAPI 3.2 are ahead; browsers need preflights; many gateways, WAFs, and proxies still run pre-2026 method allowlists. Verify every hop in your request path.
- Use it today for internal/controlled APIs; keep
GETfor simple reads and aPOSTfallback for public browser-facing endpoints — and expect the framework matrix to fill in through 2026–2027.
