HTTP Has a New Verb for Search
Explore HTTP QUERY, a safe, idempotent method for body-based search. See how caching and retries work, which clients support it, and where infrastructure blocks it.
The HTTP QUERY method, defined in RFC 10008 in June 2026, puts the query in the request body the way POST does, but it is safe and idempotent like GET. That means responses can be cached and a failed request can be retried automatically.
Most API teams know the search endpoint that starts as a tidy GET and ends up as a URL with nested filters, date ranges and sort keys URL-encoded into it. Then someone moves it to POST, and caching and retries stop working.
This article covers the problem QUERY solves, what a QUERY exchange looks like on the wire, and which parts of the stack accept it and which still reject it.
Key Takeaways
- QUERY is a safe, idempotent HTTP method that carries its query in the request body. It is standardized in RFC 10008 (June 2026).
- A server must reject a QUERY request whose Content-Type is missing or doesn’t match the body. Caches that store QUERY responses include the request body in the cache key.
- Scripts can send QUERY with
fetch(), but an HTML form withmethod="query"falls back to GET and drops the body. - A cross-origin QUERY from a browser always triggers a CORS preflight, because QUERY is not a CORS-safelisted method.
- Any server, proxy or WAF that doesn’t recognize the method can reject it before your application sees the request.
Why Do Search Endpoints Get Stuck Between GET and POST?
Neither GET nor POST works well for complex searches. GET puts every filter in the URL. POST moves the filters into the body but tells every intermediary the request might change state.
RFC 9110 asks every sender and recipient to handle URIs of at least 8,000 octets. That is a minimum, not a maximum, and HTTP sets no upper limit, so any proxy, gateway or server on the path can reject a longer URL. Long query strings also end up in browser history, server logs and bookmarks, which leaks whatever the filters contain.
POST avoids those problems, but it is neither safe nor idempotent. An idempotent request has the same intended effect whether it is sent once or ten times. RFC 9110 tells clients not to retry a non-idempotent request automatically unless they have some way to know it is idempotent in practice. As a result, caches generally don’t reuse POST responses, and gateways don’t retry a POST that fails partway through.
What Is the HTTP QUERY Method?
Under RFC 10008, a QUERY request asks the server to run the query described in the request body and send back the result, without changing anything on the server. Because of that, a client or proxy can resend or restart a QUERY without worrying that a half-finished first attempt changed state, which it can’t safely assume for POST. Here is the request on the wire (see the anatomy of an HTTP request for the parts):
QUERY /products/search HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json
{"category": "laptops", "price": {"max": 1500}, "brands": ["acme", "globex"], "sort": "price_asc"}
The query is defined by the body together with its media type. If the Content-Type header is absent or doesn’t match what the body contains, RFC 10008 requires the server to reject the request. RFC 10008’s status-code guidance points to a 4xx code such as 400 when no media type is given, and 415 when the resource doesn’t support the media type that was sent.
A response might look like this:
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=60
Accept-Query: application/json
Content-Location: /products/search/results/7f3a
Location: /products/search/queries/7f3a
{"results": [ ... ]}
Each header does a specific job:
- Cache key: QUERY responses are cacheable. A cache that stores them includes the request body in the cache key, because two QUERY requests to the same URL with different bodies ask different questions.
Accept-Query: a server sends this response header to list the query formats it accepts, written as a Structured Field list of media types. One value covers every URI on the server with the same path, whatever the query string.- Content-Location and Location: in a QUERY response, Content-Location identifies the result of that specific query. Location gives a URI for the query itself, which a client can fetch later with a plain GET. The article on what’s inside an HTTP response covers how these headers normally behave.
Cache-Control in the example is ordinary caching configuration. RFC 10008 doesn’t require it.
Where Does QUERY Work Today?
The HTTP QUERY method already works in several server runtimes, frameworks and HTTP clients, as long as nothing between client and server rejects it.
- Node.js accepts QUERY in its http module. Its bundled parser, llhttp, defines
HTTP_QUERY(see Node’s copy ofllhttp.h) and has recognized the method since llhttp 9.2. Per the Node.js changelog, that parser version shipped in v21.7.2, and it is included in v22 and later and in v20.19.2. On those versionshttp.METHODSincludes'QUERY'and a server handler seesreq.method === 'QUERY'. - Go’s HTTP client accepts any valid method token as the method string in
http.NewRequest, so it can send QUERY already:
package main
import (
"log"
"net/http"
"strings"
)
func main() {
body := strings.NewReader(`{"category":"laptops","price":{"max":1500}}`)
req, err := http.NewRequest("QUERY", "https://api.example.com/products/search", body)
if err != nil {
log.Fatal(err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
}
- Laravel: Laravel 13.19.0 adds an
Http::query()client method andquery()/queryJson()testing helpers for the QUERY verb. Routes can accept QUERY throughRoute::match(), with the test helpers added in laravel/framework PR #60662. A dedicatedRoute::query()helper has been merged into Laravel’s master branch rather than 13.x, so no 13.x release includes it.
In the browser, fetch() can send QUERY from script on the same origin. The Fetch issue tracking QUERY (whatwg/fetch#1938) points out that the Fetch Standard neither forbids QUERY nor normalizes its case. Browsers send the method string exactly as you write it, so always use uppercase:
const res = await fetch('/products/search', {
method: 'QUERY',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ category: 'laptops', price: { max: 1500 } }),
});
const data = await res.json();
What Doesn’t Support QUERY Yet?
Three things block the HTTP QUERY method today: HTML forms can’t send it, some servers and security layers reject the method outright, and cross-origin calls need a preflight.
Forms. Scripts can send QUERY with fetch(), but forms can’t. The proposal to support method="query" is WHATWG HTML issue #12594. It is still open and labelled “needs implementer interest”. Until it lands, this markup:
<form method="query" action="/products/search">
<input name="category" value="laptops">
<button>Search</button>
</form>
arrives at the server as a GET with the body dropped, because browsers treat an unknown form method as GET. A live repro built for the issue shows this.
Servers, gateways and WAFs. Any hop that doesn’t recognize the method can reject it. The same repro notes that LiteSpeed, for example, answers QUERY with a 400 before the application runs. Reverse proxies, load balancers, CDNs, API gateways and firewalls each have to be checked separately.
Cross-origin preflight. QUERY is not on the Fetch Standard’s list of CORS-safelisted methods. A cross-origin QUERY therefore always triggers a preflight, whatever headers it carries:
OPTIONS /products/search HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: QUERY
Access-Control-Request-Headers: content-type
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, QUERY
Access-Control-Allow-Headers: content-type
If Access-Control-Allow-Methods doesn’t list QUERY, the browser blocks the actual request.
| Environment | Status |
|---|---|
fetch(), same origin | Works |
fetch(), cross origin | Works after a preflight that allows QUERY |
| HTML forms | Falls back to GET, body dropped |
| Node.js http | Works (v20.19.2+, v21.7.2+, v22+; llhttp 9.2 recognizes QUERY) |
| Go HTTP client | Works (custom method string) |
| Laravel 13.19+ | Works (client, tests, Route::match) |
| LiteSpeed | Rejected with 400 before the app runs (noted in the #12594 repro) |
| Proxies, CDNs, gateways, WAFs | Depends on the product and its configuration |
Should GraphQL Use QUERY?
QUERY suits GraphQL query operations, which only read data and fit a safe, idempotent, body-carrying method. Mutations change state, so they still belong on POST. If you’re weighing the two API styles, the GraphQL vs REST explainer covers the tradeoffs. The argument for QUERY is the same in both: search-style reads get HTTP-level caching and retries back.
What Has to Happen Before QUERY Works End to End
Before QUERY works everywhere, every layer between the user and your handler has to recognize it:
- The HTML spec has to accept
method="query", and browsers have to implement it. - Each hop (origin server, reverse proxy, load balancer, CDN, API gateway and WAF) has to parse and forward the method.
- Frameworks have to route QUERY to handlers and parse its body.
- Caches have to include the request body and Content-Type in the cache key before they store QUERY responses.
The CORS preflight is by design: RFC 10008 expects it, and whatwg/fetch#1938 asks for no change to the safelist. What is still open there is whether Fetch’s URL-keyed HTTP cache should support body-keyed caching for QUERY.
Conclusion
QUERY fixes a long-standing mismatch in HTTP: search requests can carry a body without giving up caching and safe retries. Scripts, Go clients and Laravel apps can use it now. Forms can’t, and infrastructure that doesn’t recognize the method may still reject it. Before you move an endpoint over, send a real QUERY through your full production path, including CDN, gateway and WAF, and keep the POST endpoint running until every hop passes it through.
FAQs
Why not just send a request body with GET instead of using QUERY?
A GET body is unreliable. RFC 9110 Section 9.3.1 says a body on a GET has no generally defined meaning and can't change what the request targets or means. Some servers reject such requests outright, because a GET body can be used for request smuggling. The body also isn't part of the standard cache key. QUERY makes the body the query itself and puts it in the cache key.
What is the difference between the QUERY method and the WebDAV SEARCH method?
SEARCH is a WebDAV method, defined in RFC 5323 in 2008, for searching DAV resources with XML query grammars. QUERY is a general-purpose HTTP method for any query format. Both methods are safe, but SEARCH never spread beyond WebDAV. Early drafts of the QUERY specification used the name SEARCH. The authors switched to QUERY because SEARCH and the other existing safe methods come from WebDAV and rely on a generic XML media type, and because the name QUERY matches the query part of a URI.
Can I document QUERY endpoints in OpenAPI?
Yes, from OpenAPI 3.2.0, released in September 2025. That version adds built-in support for the query method, so a QUERY operation can declare a requestBody schema and responses just like get or post. Other nonstandard methods go in the new additionalOperations map. OpenAPI 3.0 and 3.1 have no field for QUERY, and tooling support for 3.2 varies from one generator to the next.
What status code does a server return when it does not support QUERY?
Under RFC 9110, a server that does not recognize a request method should respond with 501 Not Implemented. A server that recognizes QUERY but does not allow it on a particular resource returns 405 Method Not Allowed, with an Allow header listing the methods it does support. Proxies and WAFs can fail differently, for example with a plain 400, so any fallback-to-POST logic should handle all three.