The HTTP QUERY method is a request method for reading data where the question travels in the request body instead of the URL. Like GET, it is safe and idempotent: it does not change anything on the server, and a client may send it again after a failure. Like POST, it carries a body of any size and format, such as a JSON filter. The IETF published it as RFC 10008 in June 2026. Search endpoints no longer have to pick between a very long URL and a POST request that hides the fact that it only reads.
This guide shows what the method promises, how a QUERY request and its response look, and which status codes a server returns for bad queries. We also cover caching, the new Accept-Query header, and code for curl, browsers, Java, Python, Node.js and Spring.
Support is still young. A large part of the article checks which clients, servers and proxies handle QUERY in October 2026. Each claim is marked as verified or reported. Background on methods in general is in our HTTP methods article, and search design is covered in implementing search in REST APIs.
1. The Gap Between GET and POST
Before QUERY, an API had two choices for a complex search, and both had a cost. QUERY sits between them: it takes the body from POST and the meaning from GET.

| Property | GET with a long URL | POST with a body | QUERY |
|---|---|---|---|
| Query size | Limited by every system the URL passes through | Any size | Any size |
| Nested filters | Awkward as flat key-value pairs | Any format | Any format, named by Content-Type |
| Stored in logs, history and bookmarks | Yes, URLs are logged widely | Usually not | Usually not |
| Safe (reads only) | Yes | Not promised | Yes |
| Idempotent (repeat is harmless) | Yes | Not promised | Yes |
| Cacheable | Yes | Only for later GET or HEAD requests | Yes, with the body in the cache key |
1.1. Why a Long URL Causes Trouble
A filter with ten fields, a price range and a nested “or” condition turns into a URL full of escaped brackets and commas. RFC 10008 lists four problems with that approach.
- Nobody knows the real size limit in advance. A request can pass through browsers, proxies, gateways and servers that each have their own limit. RFC 9110 only recommends that they accept at least 8000 octets (bytes).
- Encoding structured data into a valid URL adds overhead, because many characters must be percent-encoded.
- URLs end up in access logs, browser history and bookmarks far more often than request bodies do.
- Every possible combination of filter values becomes a separate resource, which spreads caches thin.
1.2. What a POST Search Gives Up
Many teams move the filter into a body and call POST /products/search. That fixes the size and format problems. The cost is meaning: nothing in the request tells a client library, proxy or cache that this call only reads.
So a client may not retry it on its own after a timeout, because a repeated POST could create something twice. Shared caches do not store the answer for the next identical search. Teams often add an Idempotency-Key header to make retries safe, as described in idempotency keys in REST APIs. With QUERY, that extra machinery is not needed for reads.
2. Four Promises a QUERY Request Makes
The RFC defines QUERY through a short set of properties. Each one tells clients and intermediaries, such as proxies and caches, what they may do with the request without knowing anything about our API.
- It is safe. The client does not ask for any change to the target resource. The server may still create helper resources, such as a stored copy of the results, which section 3 explains.
- It is idempotent. Sending the same request twice has the same effect as sending it once, so it can be repeated after a dropped connection. Our guide to idempotent REST APIs explains the term in more depth.
- Its response is cacheable. A cache may reuse a stored answer for a later QUERY, but only when the body matches too.
- The body is the query. The Content-Type header names the query language, and the server must reject the request if that header is missing or does not fit the body.
The URL still names what we search, such as /products. Any query string on that URL still helps identify the resource. Whether it also changes the result is up to each API, so it is clearer to keep all search input in the body.
The body format is open. JSON filters, form-encoded strings, JSONPath expressions, SQL and XSLT all appear in the RFC examples. A public API should accept a small, documented format rather than raw SQL, as section 9 explains.
3. Reading a QUERY Exchange Line by Line
Let’s follow one product search from request to response. The example below follows the pattern of the RFC examples but uses our own data.
QUERY /products HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json
{
"filter": { "category": "laptops", "price": { "lte": 1500 } },
"sort": "price",
"limit": 2
}
The method name replaces GET or POST on the first line. The Content-Type header is required, and Accept says which response format the client wants. A successful answer looks like this:
HTTP/1.1 200 OK
Content-Type: application/json
Accept-Query: application/json
Content-Location: /products/results/42
Location: /products/queries/9f3
{
"total": 2,
"items": [
{ "id": 7, "name": "Model A", "price": 999 },
{ "id": 9, "name": "Model B", "price": 1299 }
]
}

3.1. Three Response Headers With a Special Job
Status HTTP 200 means the query ran and the results are in the body. The three extra headers are optional, and each one serves a different purpose.
| Header | What it tells the client |
|---|---|
| Accept-Query | Which query formats this resource accepts, so a client can discover them. |
| Content-Location | A URL that holds these exact results. A GET to it returns the same answer, possibly only for a short time. |
| Location | A URL that stands for the query itself. A GET to it runs the same query again, with fresh data, without sending the body. |
The difference between the last two is subtle. Content-Location points to a snapshot of results. Location points to a saved question that the server answers again on every GET. Both URLs may expire, so a client should keep the original body and fall back to a new QUERY when a stored URL fails.
3.2. How the Accept-Query Header Is Written
Accept-Query looks like Accept, but it follows the stricter Structured Fields syntax, so parsers must treat it as a list. Each item is a media type, written either bare or in double quotes, and parameters such as a charset can follow.
Accept-Query: "application/jsonpath", application/sql;charset="UTF-8"
The value applies to every URL on the server with the same path, whatever query string follows. Wildcards are limited to the two forms that match any type or any subtype, and the order of the list has no meaning. A media type that starts with a digit must be quoted, because it is not a valid bare token.
3.3. Asking Whether a Resource Supports QUERY
A client has three ways to find out before it depends on the method. The RFC shows each of them.
- Send OPTIONS to the resource and read the Allow header. A value such as GET, QUERY, OPTIONS, HEAD lists the supported methods.
- Send HEAD and read Accept-Query to learn the supported query formats.
- Try the QUERY request directly. A server without support answers HTTP 405 Method Not Allowed with an Allow header, and a server that dislikes the format answers HTTP 415.
4. Server Rules for Good and Bad Queries
Most of the server work is input checking, and the RFC is specific about the order. The server first checks the media type and only then looks at what the query asks for. It may never guess a media type from the body, a practice called content sniffing.

| Situation | Status code | What to add |
|---|---|---|
| Content-Type is missing | HTTP 400 Bad Request | A Problem Details body that names the missing header |
| Content-Type does not match the body, such as JSON declared but XML sent | HTTP 400 Bad Request | The parse error, in plain words |
| Query format not supported by this resource | HTTP 415 Unsupported Media Type | An Accept-Query header with the supported formats |
| Client asks, through Accept, for a response format we cannot produce | HTTP 406 Not Acceptable | The formats we can return |
| Query is well formed but cannot run, such as a filter on an unknown field | HTTP 422 Unprocessable Content | Which field or operator failed |
| Query ran | HTTP 200 OK | The results, plus optional Location or Content-Location |
Error bodies work best in the Problem Details format, with the media type application/problem+json. Our REST API error handling guide covers the fields. A response for an unknown filter field could look like this:
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{
"type": "https://api.example.com/problems/unknown-field",
"title": "Unknown filter field",
"status": 422,
"detail": "The field 'colour' cannot be used in a filter. Did you mean 'color'?"
}
Safety also limits what the handler may do. A QUERY handler must not start jobs, send emails or write records that count as business events. Technical logs and metrics are fine, because the client did not ask for them and does not depend on them.
4.1. Redirects Keep the Method, Except HTTP 303
Redirects behave differently from POST. After HTTP 301, HTTP 302, HTTP 307 or HTTP 308, the client sends the same QUERY with the same body to the new URL. The old browser habit of turning a redirected POST into a GET does not apply to QUERY.
HTTP 303 See Other is the exception, and it is useful. It tells the client to fetch the Location URL with a plain GET. A server can use it for expensive searches. It stores the query and returns HTTP 303 without results. The client then reads the results with GET.
HTTP/1.1 303 See Other
Location: /products/queries/9f3
4.2. Conditional Requests and Ranges
QUERY works with the usual conditional headers. A client that already holds results with an ETag can repeat the query with If-None-Match, and the server answers HTTP 304 Not Modified when nothing changed. This saves bandwidth for dashboards that poll the same search.
Byte ranges through the Range header are allowed but rarely useful for search results. Paging belongs in the query itself, for example with a limit and a cursor field. The patterns in pagination, sorting and filtering carry over unchanged, only moved into the body.
5. Caching QUERY Responses Without Mixing Up Results
Caching is where QUERY differs most from GET in practice. For GET, a cache key is mostly the URL. For QUERY, two requests to /products with different bodies are different questions. So the RFC requires the cache key to include the body and related metadata such as Content-Type.
A cache may normalize the body before it builds the key, to raise the hit rate. It may remove a content encoding such as gzip, or treat two JSON bodies that differ only in whitespace as equal. The normalization affects only the key, never the request the server receives. A client that wants no such changes can send Cache-Control: no-transform, although caches may ignore it.
Normalization has a risk the RFC calls out. If a cache decides that two bodies are equal when the server would treat them differently, it returns the wrong results. A cache that sorts JSON keys is fine; a cache that drops a field it thinks is unimportant is not. For personal results, send Cache-Control: private so that shared caches never store them, as explained in our caching guide.
Shared caches will need time. The nginx change log up to version 1.31.6 (September 2026) has no QUERY entry. The open patch that adds the method also states that it does not put the body into the cache key. Until your proxy or CDN documents body-aware keys, treat QUERY responses as cacheable only in the browser and in caches you control.
There is a simpler route to shared caching. If the server returns a Location URL for the query, clients can switch to GET on that URL, and every existing cache already understands GET.
6. Sending QUERY From Clients
Any client that lets us set a custom method name and a body can send QUERY today. One detail matters everywhere: method names are case-sensitive, so always write QUERY in capitals. In our local test with Node.js 22, a request sent with the lowercase name reached the server unchanged and was rejected with HTTP 400.
Besides that Node.js check and the Java and Python checks noted below, we did not execute these snippets. Try them against a test server before relying on them.
With curl, the -X option replaces the method and –data sends the body:
curl -X QUERY https://api.example.com/products \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
--data '{"filter":{"category":"laptops"},"limit":20}'
In JavaScript, the fetch function accepts any method that the Fetch standard does not forbid, and QUERY is not on that short list:
const response = await fetch('https://api.example.com/products', {
method: 'QUERY', // capitals: fetch does not uppercase unknown methods
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ filter: { category: 'laptops' }, limit: 20 })
});
const products = await response.json();
In Java, the built-in HttpClient from Java 11 onward takes any method name through method(). The older HttpURLConnection refuses it: on Java 21 it threw ProtocolException: Invalid HTTP method: QUERY in our check.
HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.example.com/products"))
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.method("QUERY", HttpRequest.BodyPublishers.ofString(
"{\"filter\":{\"category\":\"laptops\"},\"limit\":20}"))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
In Python, the requests library sends any method through request(), and its json argument also sets the Content-Type header:
import requests
response = requests.request(
"QUERY",
"https://api.example.com/products",
json={"filter": {"category": "laptops"}, "limit": 20},
)
print(response.status_code, response.json())
6.1. Browsers Send a Preflight First
A browser call to another origin needs extra care. QUERY is not one of the CORS-safelisted methods (GET, HEAD and POST), so the browser first sends an OPTIONS preflight request. The server must answer it with QUERY in Access-Control-Allow-Methods and Content-Type in Access-Control-Allow-Headers, or the real request never leaves the browser.
This adds one round trip per new combination of origin and URL, until the browser caches the preflight answer. CORS explained walks through the preflight step by step. Same-origin calls skip it.
7. Handling QUERY on the Server
On the server, the method passes three layers: the HTTP parser, the framework router, and any proxy or firewall in front. Here are two minimal handlers that follow the status rules from section 4.
7.1. Plain Node.js
The HTTP parser in Node.js knows QUERY, and http.METHODS includes it on Node.js 22. The handler below checks the header, parses the body and answers with the right codes. The runSearch function stands in for your own search code.
import http from 'node:http';
const server = http.createServer((req, res) => {
if (req.url !== '/products' || req.method !== 'QUERY') {
res.writeHead(405, { 'Allow': 'QUERY' });
return res.end();
}
const type = req.headers['content-type'];
if (!type) {
res.writeHead(400, { 'Content-Type': 'application/problem+json' });
return res.end(JSON.stringify({ title: 'Content-Type is required', status: 400 }));
}
if (!type.startsWith('application/json')) {
res.writeHead(415, { 'Accept-Query': 'application/json' });
return res.end();
}
let body = '';
req.on('data', chunk => { body += chunk; });
req.on('end', () => {
let query;
try {
query = JSON.parse(body);
} catch {
res.writeHead(400, { 'Content-Type': 'application/problem+json' });
return res.end(JSON.stringify({ title: 'Body is not valid JSON', status: 400 }));
}
const result = runSearch(query); // your own search code
res.writeHead(200, {
'Content-Type': 'application/json',
'Accept-Query': 'application/json'
});
res.end(JSON.stringify(result));
});
});
server.listen(8080);
Frameworks on top of Node.js vary, and we did not test them. Whether Express offers a route shortcut for QUERY depends on its version, and its issue about the method was still open in the August 2026 community tracker. A route registered with app.all() plus a method check works in any version.
7.2. Spring Framework 7.1
Spring adds HttpMethod.QUERY and RequestMethod.QUERY in Spring Framework 7.1. At the time of writing, 7.1 is available as milestone 7.1.0-M2, and the current stable release, 7.0.9, does not have the constants. There is no @QueryMapping shortcut, so we use @RequestMapping with the method set.
@RestController
@RequestMapping("/products")
class ProductQueryController {
private final ProductSearch search;
ProductQueryController(ProductSearch search) {
this.search = search;
}
// Needs Spring Framework 7.1 or later
@RequestMapping(method = RequestMethod.QUERY,
consumes = "application/json", produces = "application/json")
ResponseEntity<SearchResult> query(@RequestBody ProductFilter filter) {
return ResponseEntity.ok()
.header("Accept-Query", "application/json")
.body(search.run(filter));
}
// Fallback for clients and proxies that cannot send QUERY yet
@PostMapping(path = "/search",
consumes = "application/json", produces = "application/json")
SearchResult postSearch(@RequestBody ProductFilter filter) {
return search.run(filter);
}
}
The consumes attribute makes Spring reject other media types with HTTP 415. Check what your framework returns when Content-Type is missing altogether. The RFC expects HTTP 400 there, but some frameworks treat a missing header as an unsupported type.
The second method shows the transition plan from the next section. Both routes call the same ProductSearch service, so the results never drift apart.
8. Who Supports QUERY in October 2026
A new method works only when every hop on the path accepts it. That includes the client library, browser, gateway, firewall, load balancer, framework and API description. We checked the items marked “verified” ourselves, in official documentation or with a local test, at the start of October 2026. Items marked “reported” come from the community adoption tracker (last updated August 3, 2026) or from articles, and we could not confirm them.
| Component | Status | How we know |
|---|---|---|
| RFC 10008 | Published June 2026 as a Standards Track RFC | Verified on rfc-editor.org |
| curl | Sends any method name with -X | Verified, documented behavior |
| Node.js 22 HTTP server and fetch | Accepts and sends QUERY with a body | Verified with a local test on Node.js 22.22 |
| Browsers (fetch) | Allowed by the Fetch standard; cross-origin calls need a preflight | Standard checked, individual browsers not tested |
| Java HttpClient / HttpURLConnection | Works / rejects the method | Verified on Java 21 |
| Python requests | Sends any method name | Verified with requests 2.33 |
| OpenAPI 3.2.0 | Path items have a query operation | Verified in the specification |
| Spring Framework | RequestMethod.QUERY since 7.1, now in milestone | Verified in the 7.1 API docs |
| ASP.NET Core | HttpMethods.Query in .NET 11 previews | Verified in Microsoft Learn, preview only |
| nginx | No QUERY entry in the change log up to 1.31.6; patch open | Change log verified, patch status reported |
| Tomcat 12, Netty, Vert.x 5.2, OkHttp, Apache HttpClient, Laravel 13 | Support merged | Reported, not verified |
| Jetty, Ruby on Rails, Django, Express | Open change or discussion | Reported, not verified |
| CDNs and API gateways | No public announcement found | Not verified; test your own |
“Merged” in a tracker means the code is in a project, not that it is in the release you run. Check release notes for your exact version. The Hive Security analysis warns about older firewalls. They may block an unknown method, or let it skip rules written only for GET and POST.
8.1. A Transition Plan With a POST Fallback
For a public API, the safe plan is to offer both routes and share the code behind them. Clients that support QUERY use it, and everyone else keeps using the POST search.

- Add QUERY to the resource and advertise it with Accept-Query and the Allow header.
- Keep the existing POST /products/search route and point it at the same service.
- Describe both operations in the OpenAPI document. Tools that predate OpenAPI 3.2 will only show the POST route.
- Send real QUERY traffic through the full production path, including the CDN, gateway and firewall, before announcing it.
- Watch how many calls use each route, and retire the POST route only when your clients no longer need it.
9. Security Checks for Query Bodies
Moving search input from the URL into the body keeps it out of many access logs, browser history and bookmarks. That is a real gain for searches on names, emails or medical terms. Yet a body is still logged by some tools, so it is no place for passwords or tokens.
- Apply the same authorization as the matching GET. A user who may not list another tenant’s products may not query them either.
- Never accept a full query language from untrusted clients. A body that takes SQL or a template language invites injection. Use an allow-list of fields and operators.
- Set limits on body size, nesting depth, number of conditions and running time. Answer HTTP 413 for a body that is too large and HTTP 422 for a query that is too complex.
- Keep sensitive values out of generated Location and Content-Location URLs. The RFC says those URLs should not contain sensitive parts of the query.
- Check that your web application firewall inspects QUERY bodies the same way it inspects POST bodies.
10. Troubleshooting a QUERY That Fails
Most early failures come from a component in the middle, not from the API code. The table lists the symptoms we expect most often and where to look first.
| Symptom | Likely cause | What to do |
|---|---|---|
| HTTP 405 from the server | The framework or route does not register QUERY | Upgrade to a version with QUERY support, or route it by hand |
| HTTP 400 although the body is fine | Method sent in lowercase, or Content-Type missing | Write QUERY in capitals and always send Content-Type |
| HTTP 415 Unsupported Media Type | The query format is not accepted | Read Accept-Query in the response and switch format |
| Browser shows a CORS error | The preflight answer does not allow QUERY | Add QUERY to Access-Control-Allow-Methods |
| HTTP 501 or HTTP 403 from a proxy, CDN or firewall | The middle box blocks unknown methods | Allow the method there, or fall back to the POST route |
| Body arrives empty at the server | A client or proxy drops bodies on methods it does not know | Capture the request at each hop and replace the component that drops it |
| Two different searches return the same results | A cache keys on the URL only | Turn off shared caching for QUERY or fix the cache key |
11. GET, POST or QUERY for a New Search Endpoint
QUERY is not a replacement for every GET. Short searches stay simpler as query parameters, because links can be shared and every cache understands them.
| Your situation | Suggested choice |
|---|---|
| A few simple filters that fit in a short URL | GET with query parameters |
| Large or nested filters, and you control the clients and the network path | QUERY, with a POST fallback while support grows |
| Search input contains personal data | QUERY or POST, never the URL |
| Public API with unknown clients behind old proxies | POST search today, then add QUERY once your path is tested |
| Results should be shareable as a link | GET, or QUERY that returns a Location URL |
12. Short Answers About the QUERY Method
These questions come up most often when teams first read about the method.
12.1. Is QUERY the Same as GET With a Body?
Close, but not the same. HTTP gives a body on GET no defined meaning, so servers and proxies may ignore or reject it. QUERY defines the body as the query, requires a Content-Type, and tells caches to include the body in the cache key.
12.2. Can a Browser Send a QUERY Request?
Yes, through fetch, because the Fetch standard does not forbid the method. HTML forms cannot use it, and every cross-origin call triggers a CORS preflight. We checked the standard and Node.js, not each browser, so test the browsers your users have.
12.3. Should We Replace Our POST Search Endpoints Now?
Not yet for public APIs. Add QUERY next to the POST route, measure how clients use it, and remove POST only when your own traffic shows it is no longer needed. Internal APIs where you control every hop can move sooner.
13. Conclusion
The QUERY method gives read-only searches a proper home: a request body like POST, with the safety and caching rules of GET. Support it on the server where your framework allows it, keep a POST fallback while client tools catch up, and normalize the body before you use it as a cache key.
14. References
The RFC is the source for every rule above, and the remaining links back up the support table.
- RFC 10008: The HTTP QUERY Method
- RFC Editor: RFC 10008 information page
- RFC 9110: HTTP Semantics
- RFC 9111: HTTP Caching
- RFC 9651: Structured Field Values for HTTP
- WHATWG Fetch standard
- OpenAPI Specification 3.2.0 and the OpenAPI guide to HTTP methods
- Spring Framework 7.1 RequestMethod API
- Microsoft Learn: HttpMethods.Query
- nginx change log
- HTTP QUERY ecosystem adoption tracker
- Kreya: The new HTTP QUERY method explained
- Hive Security: HTTP QUERY attack surface
- Ruby on Rails forum: proposal to support the HTTP QUERY method
Happy Learning !!
Comments