Skip to content

How the HTTP QUERY Method Works

QUERY solves a problem every API faces: sending complex queries to a server without sacrificing safety, idempotency, or cacheability. Defined in RFC 10008 (June 2026), QUERY is an HTTP method that carries a request body — like POST — while preserving the semantic guarantees of GET.

In one sentence: QUERY is GET with a body — safe, idempotent, and cacheable by specification.


Property GET POST QUERY
Safe (no state change)
Idempotent (repeatable without side effects)
Cacheable ❌¹
Accepts request body ❌²

¹ POST responses can only be cached for future GET/HEAD requests, with severe restrictions.
² RFC 9110 states that a body in GET “has no generally defined semantics” and intermediaries may discard it.


In QUERY, the body is the query. The server processes the body content as a question — not as a state-changing command.

The RFC requires the server to reject the request if the Content-Type header is missing or inconsistent with the content. Any media type is valid: application/json, application/sql, application/graphql, application/jsonpath — the server decides what it accepts.

A server can advertise which query types it accepts via the Accept-Query header:

Accept-Query: "application/json", "application/graphql"
QUERY /api/contacts HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json
Content-Length: 76
{
"filter": {
"city": "Berlin",
"active": true
},
"limit": 25,
"offset": 0
}

This is semantically identical to asking “which active contacts exist in Berlin?” — without modifying any data on the server.


QUERY responses are cacheable, but the cache key is more complex than GET.

For GET, the cache key is straightforward: method + URI. For QUERY, the RFC mandates that the cache key MUST incorporate the request content and related metadata (such as Content-Type).

In practice, a proxy computes:

cache_key = hash(method + target_URI + normalized_body + content_type)

Caches MAY normalize the body before generating the key, removing semantically insignificant differences:

  • Strip content-encoding (e.g., decompress gzip before comparing)
  • Normalize JSON (reorder keys, remove extraneous whitespace)
  • Apply media type knowledge (a +json suffix indicates JSON normalization is safe)

Normalization never alters the actual request — only the representation used for key generation.

The server can return the Content-Digest header in the response, providing a hash of the response body. This allows clients and intermediaries to verify integrity and identify identical responses:

Content-Digest: sha-256=:4REjxQ4yrqUVicfSKYNO/cF9zNj5ANbzgDZt3/h3Qxo=:
Aspect GET QUERY
Cache key Method + URI Method + URI + Body + Content-Type
Buffering required No Yes (proxy must read the full body)
Normalization target URI parameters Request body
Equivalent resource The URI itself URI derived by incorporating the body
HTTP/1.1 200 OK
Content-Type: application/json
Content-Digest: sha-256=:Kp+BNRhIbcXKW4IA6WmUBjbOz/ia/enOVP22eCJPMjU=:
Cache-Control: max-age=300
ETag: "q-contacts-berlin-v42"
Date: Fri, 04 Jul 2026 12:00:00 GMT
Content-Length: 231
{
"results": [
{"id": 1, "name": "Anna Müller", "city": "Berlin"},
{"id": 2, "name": "Karl Weber", "city": "Berlin"}
],
"total": 94,
"limit": 25,
"offset": 0
}

An identical QUERY request within the next 300 seconds can be served directly from cache — without hitting the origin server.


The RFC resolves a historical problem: proxies and intermediaries must not discard the body of QUERY requests.

When developers attempted to send a body with GET, intermediaries (proxies, load balancers, CDNs, WAFs) would frequently:

  • Silently strip the body
  • Reject the request with an error
  • Close the connection on suspicion of request smuggling

This happened because RFC 9110 defines that a body in GET “has no generally defined semantics” — intermediaries had no obligation to preserve it.

With QUERY, the semantics are explicit: the body is the meaningful content of the request. Intermediaries:

  • MUST forward the body intact to the origin server
  • MUST include the body when computing cache keys
  • MUST NOT replay the request without the original body
  • MAY retry automatically (because QUERY is safe + idempotent)

The RFC highlights that URIs are routinely logged by intermediaries, but request bodies typically are not. QUERY allows sending filters containing sensitive data (email addresses, internal IDs, PII-bearing fields) without exposing them in access logs across the entire proxy chain.


Complete Flow: Client → Proxy → Origin → Cache

Section titled “Complete Flow: Client → Proxy → Origin → Cache”
┌────────┐ ┌───────────┐ ┌────────────┐
│ Client │ │ Proxy │ │ Origin │
└───┬────┘ └─────┬─────┘ └──────┬─────┘
│ │ │
│ QUERY /contacts │ │
│ Body: {"city":"…"} │ │
├────────────────────►│ │
│ │ │
│ ┌──────┴──────┐ │
│ │ Compute key │ │
│ │ method+URI │ │
│ │ +body+CT │ │
│ └──────┬──────┘ │
│ │ │
│ Cache miss? │
│ │ QUERY /contacts │
│ │ Body: {"city":"…"} │
│ ├─────────────────────►│
│ │ │
│ │ 200 OK │
│ │ Content-Digest:... │
│ │ Cache-Control:... │
│ │◄─────────────────────┤
│ │ │
│ ┌──────┴──────┐ │
│ │ Store in │ │
│ │ cache (key) │ │
│ └──────┬──────┘ │
│ │ │
│ 200 OK │ │
│ Content-Digest:... │ │
│◄────────────────────┤ │
│ │ │
│ │ │
│ QUERY /contacts │ │
│ Body: {"city":"…"} │ (same query) │
├────────────────────►│ │
│ │ │
│ Cache hit! │
│ 200 OK │ │
│ (from cache) │ (origin not │
│◄────────────────────┤ contacted) │
│ │ │

QUERY supports conditional requests. If the client holds an ETag from a previous query, it can send If-None-Match to check whether results have changed:

QUERY /api/contacts HTTP/1.1
Host: api.example.com
Content-Type: application/json
If-None-Match: "q-contacts-berlin-v42"
{"filter": {"city": "Berlin"}, "limit": 25}

If results are unchanged, the server returns:

HTTP/1.1 304 Not Modified
ETag: "q-contacts-berlin-v42"

Zero bytes transferred. The query was validated without retransmitting the result set.


Before (POST /search — incorrect semantics)

Section titled “Before (POST /search — incorrect semantics)”
POST /api/contacts/search HTTP/1.1
Content-Type: application/json
{"filter": {"city": "Berlin"}, "limit": 25}

Problems:

  • Proxy cannot cache (POST is potentially unsafe)
  • Automatic retry is forbidden (POST may have mutated state)
  • Semantics are dishonest — no resource is being created

Before (GET with query string — practical limitations)

Section titled “Before (GET with query string — practical limitations)”
GET /api/contacts?filter[city]=Berlin&filter[active]=true&limit=25

Problems:

  • URI exposed in logs of every intermediary
  • ~8,000 octet limit (best case)
  • Every parameter combination = distinct resource for caches
QUERY /api/contacts HTTP/1.1
Content-Type: application/json
{"filter": {"city": "Berlin", "active": true}, "limit": 25}

Advantages:

  • ✅ Safe — no state is altered
  • ✅ Idempotent — repeatable without consequences
  • ✅ Cacheable — proxy can serve from cache
  • ✅ Body preserved by intermediaries
  • ✅ Sensitive data kept out of URI logs