10 min read

The QUERY Method: HTTP Finally Has a Shape for Read-Only Requests With Bodies

RFC 10008 standardizes QUERY, a safe and idempotent HTTP method for structured request-body queries. The useful part is not that we get another verb. It is that the rest of the HTTP path can finally distinguish a read-only body-based query from an arbitrary POST.
The QUERY Method: HTTP Finally Has a Shape for Read-Only Requests With Bodies
Photo by Marsha Reid / Unsplash

There is a kind of HTTP request that modern APIs need all the time, but that the protocol has historically represented badly. A client wants to ask a server a read-only question, and the question is too structured, too large, or too awkward to fit naturally in the URI. Search, analytics, log query, GraphQL, reporting, and cloud control plane APIs all end up here sooner or later, because real queries tend to accumulate filters, projections, joins, pagination state, authorization scope, tenant boundaries, feature flags, and product-specific knobs until ?q=something&page=2 is no longer a serious representation of the request.

For years, the practical answer was to use POST. It was not really right, but it worked.

POST /search HTTP/1.1
Host: api.example.com
Content-Type: application/json

{
  "region": ["westeurope", "northeurope"],
  "sku": "premium-ssd-v2",
  "minCapacityGiB": 1024,
  "zones": ["1", "2", "3"],
  "includeUnavailable": false
}

The people close to the endpoint know what this means. The client author knows it is a search. The backend author knows it is a read. The API docs probably say it is safe. The handler may not modify anything other than the usual operational side effects like metrics, logging, tracing, quota accounting, and maybe some internal cache state. But that knowledge is trapped at the application layer. Everything generic between the client and the handler only sees POST, and POST is intentionally broad enough to mean almost anything a resource wants it to mean.

That ambiguity has always been the real problem. A POST might create an order, send an email, enqueue a workflow, submit a form, charge a card, run a search, calculate a price, or validate a policy. Some of those operations are safe to repeat. Some very much are not. Without application-specific knowledge, a generic HTTP component cannot infer which case it is looking at. That affects retries, caching, gateway policy, WAF rules, service mesh behaviour, SDK defaults, observability, synthetic tests, and how operators reason about a failed request during an incident.

RFC 10008 standardizes the QUERY method to fill exactly that gap.

QUERY /search HTTP/1.1
Host: api.example.com
Content-Type: application/json

{
  "region": ["westeurope", "northeurope"],
  "sku": "premium-ssd-v2",
  "minCapacityGiB": 1024,
  "zones": ["1", "2", "3"],
  "includeUnavailable": false
}

The change is not just spelling. QUERY says that the request target should process the enclosed content in a safe and idempotent manner and return the result of that processing. In other words, it gives a proper HTTP method to the thing people were already doing with POST: sending a body to express a read-only query.

That matters because HTTP methods are one of the few pieces of application intent that survive across the whole request path. A request can pass through a browser stack, an enterprise proxy, a CDN, a WAF, an API gateway, a load balancer, a service mesh sidecar, a reverse proxy, framework middleware, and an observability agent before it reaches the code that actually knows the route semantics. If the operation is safe and idempotent, encoding that fact in the method lets those layers make better default choices without knowing that /search, /reports/query, or /inventory/filter is special.

The obvious question is why this was not solved by GET with a body. After all, GET already has the safety semantics people want. The problem is not that no implementation can send or receive a body on GET; plenty can. The problem is that a GET request body has never had generally defined semantics in HTTP, and the surrounding ecosystem never converged on treating it as meaningful application input. Some clients make it awkward, some frameworks expose it, some intermediaries forward it, and some infrastructure ignores it, strips it, buffers it oddly, or treats it as suspicious. In a direct client-to-origin test it may appear to work, but in a real cloud path you are depending on every boring component in the middle preserving something the method does not define as significant. That is not a great foundation for a public API.

Putting everything in the URI is only clean while the query remains URI-shaped. GET /products?category=books&sort=price&page=2 is fine. Once the query turns into a nested object with long lists of filters, exclusions, projections, time windows, pagination cursors, tenant context, or ACL scope, the URI stops being a useful identifier and becomes a percent-encoded serialization format. At that point you are dealing with length limits that are not always visible ahead of time, inconsistent product caps across proxies and gateways, ugly debugging, encoding overhead, and a much larger accidental disclosure surface. URIs are copied everywhere: access logs, browser history, bookmarks, traces, metrics labels, dashboards, SIEM events, support tickets, pasted curl commands, incident notes, and screenshots. They are a poor place to hide anything sensitive or high-cardinality.

That is why POST became the universal escape hatch. It could carry a body and it worked through existing infrastructure. The industry chose deployability over precision, which is what usually happens when the protocol does not give people the primitive they need. The downside is that every read-only POST becomes an application-specific exception that lower layers cannot safely reason about.

The failure-handling case makes this concrete. Suppose a client sends a read-only query, the server processes it, and the connection dies before the response gets back to the client. Can the client retry? If the method is POST, a generic client library cannot know. Maybe this particular endpoint is harmless. Maybe it created something. Maybe it submitted something. Maybe it started a workflow. The safe answer is to avoid automatic retry unless the client has endpoint-specific policy or the request uses some application-level idempotency mechanism. With QUERY, the method itself says the request can be repeated or restarted without concern for partial state changes, because the requested operation is safe and idempotent.

That does not mean QUERY turns HTTP into RPC. The target URI still matters. A QUERY /inventory request asks the /inventory resource to perform a query within the scope of that resource. The request body describes the query, but it does not replace the resource model.

QUERY /inventory HTTP/1.1
Host: api.example.com
Content-Type: application/json

{
  "sku": "premium-ssd-v2",
  "region": "westeurope"
}

That distinction matters more than it first appears. QUERY /inventory, QUERY /inventory/disks, and QUERY /inventory?tenant=a are not automatically the same operation just because the body is the same. The resource scope, the query component of the target URI, the content type, the body, and relevant request metadata all participate in the meaning of the request. Any intermediary that wants to cache, route, authorize, or observe this traffic has to respect that.

Caching is where the new method gets both useful and dangerous. RFC 10008 makes responses to QUERY cacheable, which gives structured read APIs something they mostly lost when they moved from GET to POST. But a body-aware cache is not the same animal as a normal GET cache. With GET, a cache can usually start from the method, URI, and selected request headers. With QUERY, the cache key has to incorporate the request content and related metadata, because two requests to the same URI with different bodies are different queries.

A conservative implementation can key on the method, target URI, relevant request headers, content type, authorization-relevant state, and a digest of the exact request body. That is not necessarily optimal, but it is safe. It means these requests do not collide:

QUERY /search
Content-Type: application/json

{"tenant":"a","q":"disk"}
QUERY /search
Content-Type: application/json

{"tenant":"b","q":"disk"}
QUERY /search
Content-Type: application/json

{"tenant":"a","q":"snapshot"}

The temptation is to normalize the body to improve cache hit rate. Two JSON objects may be semantically equivalent even if their byte representation differs because of whitespace or member ordering.

{"level":"error","service":"billing-api"}
{
  "service": "billing-api",
  "level": "error"
}

A raw digest treats those as different. A JSON-aware cache could normalize them and get better reuse. That sounds attractive until the cache and origin disagree about what is semantically insignificant. If normalization creates a false miss, you waste compute. If it creates a false hit, you return the wrong response. In multi-tenant systems, that can become a data leak. A proxy that rejects QUERY loudly is irritating but safe. A cache that accepts QUERY and keys it only by method and URI is much worse.

That is probably the most important production warning in the whole RFC. Do not enable shared caching for QUERY just because the method is cacheable. Enable it only after proving that the cache key includes the request body and the metadata that changes how the body is interpreted. Until then, use conservative cache directives or keep it private.

The Location and Content-Location behaviour is also more useful than it may look at first. A server can answer a QUERY and also give the client a URI for a related resource. Content-Location can identify a resource corresponding to the representation returned in the response, while Location can identify an equivalent resource that the client can later retrieve with GET instead of resending the same query body.

HTTP/1.1 200 OK
Content-Type: application/json
Location: /queries/01JZ7Q2FW6B7QW6H8G9YZR5KXA

{
  "items": []
}

That gives API designers a bridge between body-based queries and URI-addressable resources. A client can send the complex query once, and if the server can safely materialize or canonicalize it, subsequent access can use a normal GET. That is useful for expensive reports, repeated queries, shared result sets, and workflows where a body-based request is the right input form but a stable URI is the right retrieval form.

The footgun is the same one as always: do not put sensitive query content into the generated URI. If the original request body contains tenant names, customer identifiers, email addresses, internal resource IDs, ACL scope, or security-sensitive filters, the resulting URI should be opaque. One reason to use QUERY rather than an enormous GET URI is to reduce how much query state gets sprayed into logs and tooling. Reconstructing that state into /queries/[email protected] defeats the point.

Redirect semantics are another place where QUERY is not just renamed POST. For 301, 302, 307, and 308, the client is being pointed at another URI where it can perform a similar QUERY. The old historical behaviour where some clients rewrite redirected POST requests into GET is not the model here. For 303 See Other, the server is explicitly saying the client should use GET on the URI in Location.

That makes materialized-query patterns clean.

QUERY /reports HTTP/1.1
Content-Type: application/json

{
  "month": "2026-06",
  "groupBy": "region"
}
HTTP/1.1 303 See Other
Location: /reports/2026-06/by-region

The client can then retrieve /reports/2026-06/by-region with GET. That distinction matters for client libraries, SDKs, gateway authors, and anyone implementing redirect-following behaviour. Copying old POST redirect assumptions into QUERY would be wrong.

Accept-Query is the discovery mechanism that keeps the method from pretending there is one global query language. The method says the request is a safe, idempotent query with content. The media type says how that content is interpreted by the target resource. A server can advertise supported query formats separately from normal response negotiation.

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

That matters because QUERY /objects with JSONPath and QUERY /reports with SQL-like content may both be valid uses of the method, but they are not the same protocol layered on top. The resource and media type define the query semantics. Servers should be strict here. Unsupported media types should get 415 Unsupported Media Type. Malformed bodies should get 400 Bad Request. A body that is syntactically valid but semantically unprocessable may deserve 422 Unprocessable Content. Guessing the query language from the body would reintroduce the kind of ambiguity the method is trying to remove.

The first production failures will be boring, because new HTTP methods usually fail in plumbing before they fail in theory. Browser clients making cross-origin QUERY requests need CORS preflight because QUERY is not a CORS-safelisted method. Gateways with hardcoded GET, POST, PUT, PATCH, DELETE, OPTIONS allowlists need updating. WAF rules may classify unknown methods as suspicious. API gateway route matchers, service mesh policies, synthetic monitors, OpenAPI tooling, SDK generators, metrics dimensions, log parsers, and incident dashboards may all have assumptions about the set of “real” methods. None of that is conceptually difficult, but every one of those places can break a rollout.

That is why I would not start by replacing every public POST /search endpoint with QUERY. The first good deployment target is controlled service-to-service traffic where you own the client, the gateway, and the origin. Fleet inventory, metadata lookup, policy evaluation, control-plane search, reporting, and log query APIs are good candidates because they already tend to have read-only POST endpoints and they benefit from explicit retry semantics.

A sane migration keeps the old POST route as the compatibility path and adds QUERY as the semantically correct path.

POST /inventory/search
Content-Type: application/json
QUERY /inventory
Content-Type: application/json

After that, the real work is not the handler. The real work is making the path honest. Route QUERY explicitly at the gateway. Allow it explicitly in the WAF. Add it to CORS where browser clients need it. Teach observability to treat it as a first-class method rather than dumping it into OTHER. Disable shared caching until the cache-key behaviour is proven. Add request body digests to telemetry so cache behaviour can be debugged without logging raw query bodies. Test retry behaviour by killing connections after request upload and before response delivery. Test redirects separately from POST. Test missing content type, unsupported media type, malformed content, and semantically invalid queries.

None of that is glamorous, but it is the difference between supporting RFC 10008 and merely adding a method that works on the happy path.

There is also an application-level trap: safe does not mean cheap. A QUERY request can be read-only and still melt a database. A log search can be idempotent and still scan terabytes. A reporting query can have no side effects and still saturate the cluster. The method gives infrastructure a better signal about side effects and retryability, but it does not solve query planning, admission control, quotas, timeouts, pagination, cancellation, or abuse protection. In fact, making structured read queries easier to express may increase pressure on those controls because clients will naturally send richer queries.

That is the practical shape of QUERY. It is not a transport revolution. It does not change HTTP framing, TLS, congestion control, head-of-line blocking, or the realities of middleboxes. It is a small addition to the method registry, but small method-level signals matter because they are visible to the whole path. POST became overloaded because it was the only universally deployable way to send a body. QUERY gives a large class of read-only body-based requests a more honest representation.

Use GET when the URI cleanly identifies what is being retrieved. Use POST when the request may create, submit, enqueue, trigger, mutate, charge, or otherwise ask the server to process something with side effects. Use QUERY when the client is asking a safe, idempotent question and the question belongs in the request body.

The useful thing about RFC 10008 is not that HTTP gets another verb. It is that a whole category of APIs can stop pretending their read-only queries are arbitrary submissions, and the network can finally tell the difference.