← Back to blog
httpapi-designjavaspringbackend

HTTP QUERY (RFC 10008): The Method That Finally Fixes GET vs POST for Search

RK

Ravi Kumar

July 14, 2026 · 14 min read

Quick answer: HTTP QUERY is a request method that carries a body like POST but is safe and idempotent like GET. It was published as RFC 10008 in June 2026, so it is no longer a draft. You send your filter payload in the request body, the server treats it as a read, and caches and intermediaries are allowed to cache the response and retry the request. curl and the JDK HttpClient can send it today, and Spring MVC can be made to handle one with a small workaround. Browsers cannot send it natively yet, so for public APIs it is still early.

Every backend engineer has had this argument at least once. You have a search endpoint. The filter payload is a nested object with date ranges, an array of statuses, and a sort spec. It does not fit comfortably in a query string, and even if you force it in, you are one more filter away from a proxy quietly truncating your URI.

So you reach for POST. Then someone on the review says "but this is a read, POST is wrong," and they are right, and you ship it anyway because there is no third option.

Now there is.

What is the HTTP QUERY method?

QUERY asks the target resource to process the content of the request body and respond with the result of that processing. The spec describes it as similar to POST, except that QUERY requests can be automatically repeated or restarted without concern for partial state changes.

That is the whole idea in one sentence. Body from POST, semantics from GET.

QUERY /users HTTP/1.1
Host: example.org
Content-Type: application/json
 
{ "role": "admin", "active": true, "sort": "name" }
http

QUERY is now in the IANA HTTP Method Registry with safe and idempotent both set to yes. That is not just documentation. It is the signal that lets a cache store the response and lets an intermediary retry a failed request without asking you whether that is okay.

Why not just use GET or POST?

Because each one gives you exactly half of what you need.

GET is safe, idempotent, and cacheable, but everything has to live in the URI. The RFC is blunt about why that hurts. Size limits are not knowable ahead of time, because a request passes through many uncoordinated systems, and the baseline recommendation in HTTP Semantics is only that senders and recipients support at least 8000 octets. Request URIs get logged far more eagerly than request bodies, which matters the moment your filter contains an email address or an account number. And encoding every filter combination into the URI effectively casts every possible combination of inputs as a distinct resource, which is a strange thing to say about one search endpoint.

POST solves the size problem and the logging problem. What it cannot do is tell anyone that the operation is a read. Without specific knowledge of the resource and server, nothing in the protocol says this request is safe. So caches skip it, and retry logic has to treat it as dangerous.

There is a table in the RFC that makes the gap obvious. POST responses are cacheable only for future GET and HEAD requests. QUERY responses can satisfy a subsequent QUERY. GET gives you a URI for the query by definition. QUERY gives you one optionally, via Location. POST gives you neither.

QUERY is the missing quadrant. Body, and safe.

Does QUERY actually work today? I tried it

I wanted to see this for myself, because "it is a new standard, go use it" is not a claim you should accept from a blog post, including this one.

I wrote a tiny Node server that handles QUERY on /users, plus OPTIONS and HEAD for discovery:

const server = http.createServer((req, res) => {
  if (req.method === 'OPTIONS') {
    res.writeHead(204, { Allow: 'OPTIONS, HEAD, GET, QUERY' });
    return res.end();
  }
  if (req.method === 'HEAD') {
    res.writeHead(200, { 'Accept-Query': 'application/json' });
    return res.end();
  }
  if (req.method !== 'QUERY') {
    res.writeHead(405, { Allow: 'OPTIONS, HEAD, GET, QUERY' });
    return res.end();
  }
  if (req.headers['content-type'] !== 'application/json') {
    res.writeHead(415, { 'Accept-Query': 'application/json' });
    return res.end();
  }
  // ...read the body, filter, respond
});
js

Node's core http module did not care that QUERY is a new method. It hands you req.method as a string and gets out of the way.

curl did not care either:

curl -i -X QUERY http://localhost:8080/users \
  -H 'Content-Type: application/json' \
  -d '{"role":"admin","active":true}'
bash
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=60
Content-Location: /users/results/1
 
[{"id":1,"name":"Asha","role":"admin","active":true}]
http

Then the one I actually cared about, since I write Java for a living. java.net.http.HttpClient accepts arbitrary method tokens through method(String, BodyPublisher), which the old HttpURLConnection never did:

HttpRequest req = HttpRequest.newBuilder(URI.create("http://localhost:8080/users"))
    .header("Content-Type", "application/json")
    .method("QUERY", HttpRequest.BodyPublishers.ofString("{\"role\":\"admin\"}"))
    .build();
 
HttpResponse<String> res = HttpClient.newHttpClient()
    .send(req, HttpResponse.BodyHandlers.ofString());
java

On Java 21 that returned 200 with both admin rows, since this one filters on role alone. No patching, no reflection hack, no custom transport. The JDK put QUERY on the wire like it was any other verb.

Sending a QUERY is a solved problem. Receiving one is where your framework starts to matter.

Can Spring MVC handle a QUERY request?

Partly, and the details are better than a yes or no. I traced this through the Spring Framework 6.2.0 source rather than guessing.

The good news: the request reaches your DispatcherServlet. FrameworkServlet.service() keeps a set of the methods plain HttpServlet knows about, which is DELETE, HEAD, GET, OPTIONS, POST, PUT, TRACE. Anything outside that set skips super.service() entirely and goes straight to processRequest:

if (HTTP_SERVLET_METHODS.contains(request.getMethod())) {
    super.service(request, response);
}
else {
    processRequest(request, response);
}
java

That else branch is why QUERY does not die with a 501 the way a raw servlet answers an unknown verb. Spring hands it to the normal dispatch pipeline.

The bad news: you cannot declare it. RequestMethod is still an enum with exactly eight constants, GET through TRACE, and QUERY is not one of them. So @RequestMapping(method = ...) has nothing to bind to.

The gap in between is the useful part. Look at how the method condition actually matches. If a mapping declares no methods at all, the condition is empty and getMatchingCondition returns itself, which counts as a match. If it does declare methods, Spring calls RequestMethod.resolve("QUERY"), gets null, and returns no match.

Which means a path-only mapping matches a QUERY request today:

@RequestMapping("/users") // no method attribute, so any verb matches
public List<User> query(@RequestBody UserFilter filter, HttpServletRequest request) {
    if (!"QUERY".equals(request.getMethod())) {
        throw new ResponseStatusException(HttpStatus.METHOD_NOT_ALLOWED);
    }
    return userService.search(filter);
}
java

@RequestBody binds, argument resolution works, the whole thing works. You are just doing the method check by hand, and you do need that check, because a path-only mapping will happily catch GET and POST to the same path too. If you want it to feel native, the real fix is a custom request condition or a RequestMappingHandlerMapping subclass, which is a contained afternoon rather than a blocker.

On the client side Spring is already fine, because HttpMethod stopped being an enum in Spring 6. HttpMethod.valueOf falls through to new HttpMethod(method) for anything it does not recognise, so RestClient and WebClient can send HttpMethod.valueOf("QUERY") with no ceremony.

One caveat: I read this out of the 6.2.0 source, I did not boot a Spring app to confirm it end to end. Verify against the version you are actually on before you write it into a design doc.

What do the QUERY response headers mean?

This is where most write-ups stop early, and it is the genuinely useful part of the spec.

Accept-Query is a response header that advertises which query media types a resource understands. Put it on your HEAD or OPTIONS responses and clients can discover your formats instead of guessing:

Accept-Query: "application/jsonpath", application/sql;charset="UTF-8"
http

It looks like Accept, but it is not. It is a Structured Field, specifically a List of Tokens or Strings, so it must be parsed as one. Media type parameters map to Structured Field parameters. Media types with a leading digit cannot be Tokens, which is why the String form exists.

Content-Location on a successful QUERY identifies a resource holding the results of the query you just ran. The client can GET that URI to fetch those results again. The server is allowed to make it temporary, so treat a later failure as a cue to redo the QUERY.

Location means something different, and this is the pair people conflate. It points at the equivalent resource, a URI that will re-run the same query on GET and give you a current result, without you resending the body. Content-Location is the answer you got. Location is the question, saved.

303 See Other goes one step further. The server returns no results at all, just a Location to GET. Useful when the server wants to materialise an expensive query once and let you pull it.

One redirect subtlety worth knowing: the historical exception where a 301 or 302 turns a POST into a GET does not apply to QUERY. A redirected QUERY stays a QUERY.

And the error codes are specified with unusual care:

  • No Content-Type at all: 400, because a QUERY without a media type is incorrect by definition
  • Media type not supported: 415, plus Accept-Query so the client knows what to try instead
  • Media type declared but inconsistent with the actual content: 400, and no content sniffing to paper over it
  • Media type understood and content consistent, but the query itself cannot be processed, like a valid SQL string against a table that does not exist: 422
  • Client asked via Accept for a response type you do not produce: 406

My little Node server returns 415 with Accept-Query on a text/plain body, which took four lines and makes the endpoint self-describing.

What about caching?

This is the payoff, and it comes with a catch.

A QUERY response is cacheable, and a cache may use it to satisfy later QUERY requests. But the cache key must incorporate the request content, not just the URI. That is a real change in how a cache has to work, because it cannot compute a key until it has read the whole body.

Caches are allowed to normalise the content first to improve hit rates, by stripping content encodings or normalising a +json payload, and the security considerations flag the obvious hazard: normalise differently from how the resource interprets the query and you get a false-positive cache hit, which is a correctness bug wearing a performance costume.

The RFC's own suggested escape hatch is nice. If the response carries Location, the client can switch to plain GET on that URI for subsequent runs, and now you are back to ordinary cacheable GETs with ETags and If-None-Match.

Should you use QUERY in production yet?

Depends entirely on who is calling your API.

Internal service to service, on infrastructure you own: seriously consider it. Both ends are yours, your clients are JVM or Node or Go, and every one of those can send an arbitrary method. You get correct semantics and cacheable reads for a search endpoint that has been lying about being a write for years.

Public API with browser clients: not yet. QUERY is not CORS-safelisted, so every cross-origin call needs a preflight, and browser support has not landed. That alone rules out a lot of front ends.

Anything behind infrastructure you do not control: test first. Load balancers, WAFs, CDNs, and API gateways all have opinions about unknown methods, and plenty of them still answer 405 or 501 before your code ever runs. Ironically that is the same class of uncoordinated-middlebox problem QUERY was designed to solve, and it is exactly what will slow QUERY's own adoption.

There is precedent worth respecting here. The method registry already had three safe, idempotent methods with bodies: PROPFIND, REPORT, and SEARCH. Early drafts of this spec even used the name SEARCH. They all came out of WebDAV, and none of them ever made it into everyday API design. QUERY has a much better shot, because it is a general HTTP method rather than a WebDAV extension, but "the RFC is published" and "the ecosystem is ready" are different dates on the calendar.

The honest summary

QUERY fixes something that has annoyed API designers for a decade. GraphQL endpoints in particular have been POSTing read-only operations forever, purely because the query does not fit in a URI. QUERY removes the workaround.

It is also, right now, a standard running ahead of its ecosystem. The RFC is done. The clients are mostly ready. The frameworks, browsers, and middleboxes are not. Adopt it where you own both ends, watch it everywhere else, and stop calling it a draft in your architecture docs, because since June 2026 it has been a Proposed Standard with a number.

FAQ

Is HTTP QUERY an official standard?

Yes. It was published as RFC 10008 in June 2026 on the IETF Standards Track, authored by Julian Reschke (greenbytes), James M. Snell (Cloudflare), and Mike Bishop (Akamai). Before that it lived for years as draft-ietf-httpbis-safe-method-w-body, which is why plenty of older articles still call it a draft.

Is QUERY safe and idempotent?

Yes, both. That is the entire point. IANA lists it in the HTTP Method Registry with safe and idempotent set to yes, which is what allows caches to store QUERY responses and intermediaries to retry them after a connection failure.

Are QUERY responses cacheable?

Yes, and this is the concrete win over POST. A POST response is only cacheable for future GET or HEAD requests. A QUERY response can be used to satisfy a subsequent QUERY. The catch is that the cache key must include the request content, so the cache has to read the whole body before it can look anything up.

Can browsers send QUERY requests?

Not natively at the moment. QUERY is not a CORS-safelisted method, so any cross-origin QUERY needs a preflight OPTIONS request, and browser support has not arrived. For now, treat QUERY as a server-to-server method.

Can Java send a QUERY request?

Yes. Java 11 and later ship java.net.http.HttpClient, whose builder takes an arbitrary method token via .method("QUERY", bodyPublisher). I ran exactly that on Java 21 against a local server and got a normal 200 back. The older HttpURLConnection restricted you to a fixed method list, which is the source of a lot of "Java cannot do this" folklore.

Does Spring Boot support the QUERY method?

Not as a first-class verb, but it is closer than you would think. FrameworkServlet routes any non-standard method straight into the DispatcherServlet instead of returning 501, and a @RequestMapping that declares no method matches any verb, so a QUERY request with a JSON body reaches your handler and binds to @RequestBody. What you cannot write is method = RequestMethod.QUERY, because that enum has no QUERY constant. For sending, HttpMethod.valueOf("QUERY") works fine with RestClient and WebClient.

What is the difference between QUERY and the URL query string?

They are unrelated despite the name. The query string is the part of a URL after the ?. QUERY is a request method, like GET or POST, and its input travels in the request body instead of the URL.

Why is it called QUERY and not SEARCH?

The registry already had PROPFIND, REPORT, and SEARCH as safe, idempotent methods, and early versions of this spec did use SEARCH. The working group moved to QUERY because those alternatives all lean on a generic XML media type, they all carry WebDAV baggage, and QUERY lines up neatly with the query component of a URI.