One of API design's most familiar workarounds may finally have an expiration date.
For years, API designers have quietly made the same compromise.
They start with a clean GET request:
GET /products?category=laptop&brand=acme HTTP/1.1
Host: api.example.com
Accept: application/json
Then the product team asks for price ranges, multiple brands, availability rules, nested attributes, full-text search, sorting, pagination, and user-specific constraints.
The URL begins to look less like an address and more like a programming language:
/products
└── filters
├── category = laptop
├── brands = [acme, contoso, globex]
├── price
│ ├── minimum = 800
│ └── maximum = 1800
├── specifications
│ ├── memory >= 32 GB
│ └── storage = SSD
└── sort = rating desc, price asc
So the team moves the filters into JSON and sends them with POST:
POST /products/search HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json
{
"brands": ["acme", "contoso", "globex"],
"price": { "min": 800, "max": 1800 },
"specifications": {
"memoryGb": { "gte": 32 },
"storageType": "ssd"
},
"sort": ["-rating", "price"]
}
The endpoint works. The JSON is readable. The server returns products. But the method is now vague: without endpoint-specific knowledge, POST could mean a safe search, a payment, a message submission, or some other state-changing command.
In June 2026, RFC 10008 gave that operation its own method:
QUERY /products HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json
{
"brands": ["acme", "contoso", "globex"],
"price": { "min": 800, "max": 1800 },
"sort": ["-rating", "price"]
}
QUERY gives complex reads request content without giving up explicit read-only, retry, and caching semantics.
HTTP Methods Are Tiny Words With Large Contracts
GET, POST, PUT, and DELETE are method tokens with standardized meanings. Those meanings let clients, servers, caches, proxies, gateways, and monitoring systems reason about a request before they understand the application behind it.
The method is the first line of the contract:
HTTP request
├── method: What kind of operation is this?
├── target: Which resource is in scope?
├── fields: What metadata applies?
└── content: What representation accompanies the request?
Three properties matter to the QUERY story:
| Property | What it actually means | What it does not mean |
|---|---|---|
| Safe | The client does not request a change to the target resource's state | The server performs no work, writes no logs, or creates no auxiliary resources |
| Idempotent | Repeating the same request has the same intended effect as sending it once | Every response must contain identical data |
| Cacheable | A response may be stored and reused under HTTP caching rules | Every CDN will cache it automatically |
Idempotent does not mean identical responses. GET /products/42 can return \$900 today and \$850 tomorrow; GET remains idempotent because repeated reads request no additional state change. PUT and DELETE are also idempotent despite changing state. POST is not idempotent by definition because replaying it may repeat an action. PATCH may be idempotent in a specific use, but HTTP does not guarantee it.
The Gap Between GET and POST
GET is an excellent fit when query input fits naturally in the target URI:
GET /feed?q=http&limit=10&sort=-published HTTP/1.1
Host: example.org
It clearly communicates a safe, idempotent retrieval. Its responses work naturally with browsers, shared caches, CDNs, bookmarks, links, and observability tools.
Complex queries expose four practical limits:
- Clients, proxies, gateways, and servers impose different limits on request targets. **RFC 9110* recommends support for at least 8,000 octets, but your path is only as tolerant as its most restrictive hop.*
- Nested filters, arrays, ranges, and query languages become awkward once they are flattened via encoding into a query string.
- Request URIs often appear in logs, browser history, bookmarks, referrers, and monitoring tools. Request content can also be logged, but it is less widely exposed by default.
- With GET, every input combination becomes a distinct URI. That is useful for small filters and clumsy for large query documents.
POST avoids those limits because it expects request content. It also loses the generic signal that the operation is a safe, idempotent query.
That is the gap QUERY fills.
What QUERY Actually Means
RFC 10008 defines QUERY as a method that asks the target resource to perform a server-side query within that resource's scope. The request's content and Content-Type define the query. The target URI defines the resource being queried.
QUERY /contacts HTTP/1.1
Host: example.org
Content-Type: application/jsonpath
Accept: application/json
$[?(@.country == "India" && @.status == "active")]
Here, /contacts establishes the scope. application/jsonpath tells the server how to interpret the content. The JSONPath expression describes the query.
A server must reject a QUERY request when Content-Type is missing or inconsistent with the content. That requirement is not decorative. The same bytes could have completely different meanings under application/json, application/sql, or application/jsonpath.
QUERY is not simply “GET with a body”
That phrase is a useful first approximation, but it hides the semantic difference.
- GET asks for a current representation of the resource identified by the target URI.
- QUERY asks the target resource to perform the operation described by the request content.
HTTP assigns no generally defined semantics to content in a GET request. Some implementations reject it, some ignore it, and intermediaries cannot safely assume what it means.
| Capability | GET | QUERY | POST |
|---|---|---|---|
| Safe by definition | Yes | Yes | No |
| Idempotent by definition | Yes | Yes | No |
| Request content | No generally defined semantics | Expected | Expected |
| Query identified entirely by URI | Yes | No | No |
| Response can satisfy the same method later | Yes | Yes | No |
| Best fit | Ordinary retrieval and compact filters | Complex, read-only queries | Submissions and application-defined processing |
POST responses can be cached under specific conditions, but a stored response generally cannot satisfy a later POST. By design, a cached QUERY response can satisfy an equivalent later QUERY.
The Most Important Part: Caching the Content
A cache normally starts with the method and target URI when matching requests. That is insufficient for QUERY.
These two requests target the same URI but ask different questions:
QUERY /products HTTP/1.1
Content-Type: application/json
{"brand":"acme"}
QUERY /products HTTP/1.1
Content-Type: application/json
{"brand":"globex"}
If a cache considered only /products, it could return Acme results to a client asking for Globex. RFC 10008 therefore requires a QUERY cache key to incorporate the request content and related metadata.
A cache may normalize semantically insignificant differences before generating the key. For example, these JSON documents might mean the same thing:
{"brand":"acme","inStock":true}
{
"inStock": true,
"brand": "acme"
}
But normalization is dangerous when the cache and application disagree about meaning. A false match can return the wrong data. The safest initial implementation is often a deterministic query format plus a hash of the exact content and relevant metadata.
This is why “QUERY is cacheable” does not mean “every CDN supports it today.” A compliant QUERY cache must read the full request content and include it in matching.
A Query Can Become a GET-able Resource
RFC 10008 offers another useful pattern. A server can assign a URI to the query itself, to the result that was just produced, or to both.
HTTP/1.1 200 OK
Content-Type: application/json
Location: /products/stored-queries/42
Content-Location: /products/stored-results/17
ETag: "result-17"
[
{ "id": 7, "brand": "acme", "price": 1299 },
{ "id": 9, "brand": "contoso", "price": 1499 }
]
The two response fields serve different purposes:
| Field | Identifies | A later GET means |
|---|---|---|
Location |
An equivalent resource representing the query and its target | Retrieve the current result equivalent to executing that query |
Content-Location |
A resource corresponding to the result just returned | Retrieve that result representation |
The distinction matters when the underlying data changes. A stored query can produce a newer answer. A stored result can represent the answer from a particular execution.
QUERY also supports conditional requests. Once a client has an ETag or Last-Modified validator, it can ask for results only if they changed. If the server supplies a Location for an equivalent resource, subsequent GET requests can make that flow simpler and more widely compatible.
Safe Retries Are a System Design Feature
Imagine a client sends a complex search and the connection drops before the response arrives.
With an application-specific POST endpoint, the client cannot infer from the method alone whether retrying might repeat a state change. It needs out-of-band knowledge of that endpoint's behavior.
QUERY carries the answer in the protocol. It is idempotent, so clients and intermediaries can repeat it after a connection failure without introducing a second requested state change.
Connection fails
├── POST
│ └── Retry only with endpoint-specific knowledge or safeguards
└── QUERY
└── Retry is allowed by the method's idempotent semantics
Servers Can Advertise QUERY Support
The RFC also defines the Accept-Query response field. A resource can use it to advertise the media types it accepts as query content:
HEAD /products HTTP/1.1
Host: api.example.com
HTTP/1.1 200 OK
Accept-Query: application/json, application/jsonpath
This tells the client that the resource supports QUERY with JSON or JSONPath content. Content-Type identifies the query format, while Accept lists the desired response formats.
Request format Response format
Content-Type: application/json Accept: application/json
│ │
└── how to read the query └── how to return the result
Why You Should Not Replace Every GET Tomorrow
QUERY is now standardized. Standardization and universal deployment are not the same event.
An HTTP request usually crosses a chain of components:
Client
└── SDK or browser
└── corporate proxy
└── CDN or edge cache
└── WAF
└── API gateway
└── application server
└── route handler
Every component must either understand QUERY or pass it through correctly. Before adopting it, verify the boring path:
- The client can send an extension method with content.
- Proxies, firewalls, and gateways do not reject or rewrite it.
- The application server can route it.
- Cache behavior includes content and relevant metadata in the key.
- Authentication and authorization are evaluated before query execution.
- Logs and tracing systems label it correctly without exposing sensitive content.
- Rate limits account for query cost, not merely request count.
- Cross-origin browser calls complete the required CORS preflight.
There is also a security misconception worth removing:
Request content is less likely than a URI to appear in routine logs. It is not automatically private.
Use HTTPS. Redact sensitive fields. Validate the query language. Apply authorization at the data level. Limit depth, joins, result size, and execution time. A beautifully semantic method can still carry an expensive or malicious query.
The Bigger Lesson Is Not About One New Word
HTTP methods help distributed systems communicate intent. GET says, “Give me a representation of this resource.” POST says, “Process this content according to this resource's rules.” QUERY now says, “Safely process this query and give me the result.”
Simple filters should remain GET requests. Existing POST search endpoints will not suddenly stop working. But when a read operation becomes too expressive for a URI and your request path supports it, developers no longer have to choose between readable content and honest HTTP semantics.
The gap finally has a method.
The one sentence to remember: QUERY is a safe, idempotent, content-bearing HTTP method for complex server-side queries.



Top comments (0)