BM
Bhavik Mehta
Contact Me
Back to Blog
{ 07 } — Engineering

HTTP QUERY Method: The GET With a Body You Wanted

2026-07-0211 min read
#Engineering#HTTP#API Design#RFC 10008

What Is the HTTP QUERY Method?

The HTTP QUERY method (standardized in RFC 10008, June 2026) is a request method that carries a body like POST but is safe and idempotent like GET. It lets a client send a large, structured, read-only query in the request body — with proper caching and automatic-retry semantics — instead of cramming it into the URL or abusing POST. It is the first new standards-track HTTP method since PATCH in 2010.

If you came here searching for "GET with a body," here is the correction that matters: GET was never syntactically forbidden from carrying a body. RFC 9110 §9.3.1 is blunt about it — "content received in a GET request has no generally defined semantics." The problem was never the wire format. It was that no intermediary agreed on what the body meant, so proxies dropped it, caches ignored it, and CDNs sometimes rejected the request outright. QUERY is the fix, and it is not a patch to GET. It is a whole new verb.

The Problem: Every Search Endpoint Is Already Lying

Look at any non-trivial search API and you will find one of two hacks.

The first is the giant URL. You take a structured filter — facets, ranges, nested boolean clauses, a geo bounding box — serialize it into query parameters, and pray. It works until it doesn't. Browsers cap URLs around 2,000 characters in practice. nginx defaults large_client_header_buffers to 8KB across all headers combined, and the request line counts against it. Exceed it and you get a 414 URI Too Long — a failure mode that only appears in production, only for your power users, and only for the exact complex queries that matter most. URL-encoding a JSON filter also triples its size and turns your access logs into an unreadable smear of %7B%22.

The second hack is POST /search. This is what everyone actually ships. Elasticsearch's _search, GraphQL, most internal RPC-over-HTTP — all POST. It works, but you are lying to the entire HTTP stack about what the request does. POST is neither safe nor idempotent. That single semantic lie has real costs:

  • A CDN or reverse proxy will never cache the response, because POST is assumed to mutate state.
  • A client library, service mesh, or gateway will never transparently retry a failed POST, because retrying a non-idempotent request could double-charge a card or double-post a comment. Your read query pays the same "don't retry me" tax as a payment.
  • A crawler or prefetcher correctly refuses to touch it, so legitimate read paths lose optimizations that GET gets for free.

You are performing a read, but every layer of infrastructure treats it as a write. That mismatch is the hole QUERY was designed to fill.

The Mental Model: Safety and Idempotency Are Promises to Intermediaries

The core insight of QUERY is that HTTP method semantics are not for you. They are a contract with every box between your client and your server.

Safe means the request has no intended side effects — it is a read. Safety is what tells a prefetcher, "you may fire this speculatively." Idempotent means firing the request N times has the same effect as firing it once. Idempotency is what tells a retry layer, "if the connection drops mid-flight, just send it again." GET carries both promises. POST carries neither. That is the entire reason infrastructure treats them so differently — not the presence of a body.

QUERY makes a precise claim: here is a request with a body, and I promise it is both safe and idempotent. In the RFC's words, "the QUERY method is used to ask the target resource to perform a query operation ... QUERY is both safe and idempotent." Once an intermediary trusts that promise, it can cache the response, retry on failure, and coalesce duplicate in-flight requests — all the machinery GET enjoys, now available to a request whose parameters live in the body where they belong.

The body is where the second design decision lives. Unlike GET, QUERY requires you to describe what the body is. Servers MUST fail the request if Content-Type is missing or inconsistent with the content. The media type defines the query semantics — application/sql, application/graphql, application/json, whatever you register. The server cannot guess and cannot override your declared type. That strictness is the price of making bodies mean something on a method where, on GET, they meant nothing.

The Implementation: What a QUERY Looks Like on the Wire

Here is the shape of it. A faceted product search that would be miserable as a URL:

QUERY /products HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json
 
{
  "filter": {
    "category": "laptops",
    "price": { "gte": 800, "lte": 2400 },
    "specs": { "ram_gb": { "gte": 16 }, "gpu": ["rtx-4070", "rtx-4080"] }
  },
  "sort": [{ "field": "price", "order": "asc" }],
  "page": { "size": 40, "cursor": "eyJvZmZzZXQiOjEyMH0" }
}

No encoding. No length ceiling. No lie about what the request does. The server processes it and responds:

HTTP/1.1 200 OK
Content-Type: application/json
Content-Location: /products?q=sha256-9f2c...a71b
Cache-Control: max-age=300
 
{ "results": [ /* ... */ ], "total": 118 }

The Content-Location header is the clever part. It is a claim from the server that "a client can send a GET request for the indicated URI to retrieve the results." The server has minted a stable, cacheable URL that represents this specific query's results. Your client can bookmark it, hand it to a CDN, or switch to plain GET for subsequent polls — sidestepping the need to re-read the body on every cache lookup.

On the server, wiring it up is mostly routing. Most frameworks let you register an arbitrary method token today:

// Express-style — QUERY is just another method string
app.use((req, res, next) => {
  if (req.method === "QUERY" && req.path === "/products") {
    return handleProductQuery(req, res); // read req body, run search
  }
  next();
});

On the client, fetch in current runtimes accepts the method as a string. No new API surface — the plumbing already generalizes over method tokens:

const res = await fetch("https://api.example.com/products", {
  method: "QUERY",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(filter),
});

The Failure Modes: Where This Bites in Production

This is the section most write-ups skip, so read it before you ship QUERY anywhere near a load balancer.

Intermediaries that don't know the token will drop or reject you. This is the big one. Support is, in the RFC authors' framing, going to take years to reach everywhere. An older proxy, a corporate egress firewall, or a WAF configured with an allowlist of methods will see QUERY and return 405 Method Not Allowed — or worse, silently strip the body the way they historically did to GET. You inherit the exact fragmentation that made GET-with-a-body unusable. The mitigation is method-override negotiation or a capability check before you rely on QUERY on public paths.

CORS makes QUERY a preflighted method. QUERY is not on the CORS-safelisted method list, so any cross-origin browser request triggers an OPTIONS preflight. If your server doesn't answer preflight with Access-Control-Allow-Methods: QUERY, the browser blocks the real request and you get a CORS error with no useful body — a classic "works in curl, fails in the browser" trap. Every cross-origin QUERY endpoint needs explicit preflight handling.

Caching is genuinely harder than GET caching, not just different. For GET, the cache key is the URL — cheap, already in the request line. For QUERY, "the cache key ... MUST incorporate the request content," which means the cache has to read and normalize the entire body before it can even decide hit or miss. Two logically identical queries with keys in a different JSON order are, byte-for-byte, different requests. Unless your cache applies media-type-aware normalization, you will get near-zero hit rates while thinking caching is on. This is exactly why the Content-Location-to-GET handoff exists — lean on it for anything you actually want cached at the edge.

Missing or wrong Content-Type is a hard failure, by spec. Send a QUERY with no Content-Type and a conforming server returns 415 or 400. There is no lenient fallback the way there is for a bodyless GET. If your client library omits the header when the body is empty, you will get intermittent 4xx that vanish the moment you add a payload.

The Tradeoffs: When Not to Reach for QUERY

QUERY is the right tool for exactly one shape of problem: a read whose parameters are too large, too structured, or too sensitive for a URL. Outside that shape, it is the wrong choice.

If your query fits comfortably in a URL, keep using GET. A bookmarkable, linkable, trivially cacheable GET /products?category=laptops beats QUERY on every axis that matters for a simple read, and it works through every proxy built in the last thirty years. Do not migrate working GET endpoints for aesthetics.

If your request actually mutates state, it is a POST or PUT and no amount of body ergonomics changes that. QUERY's entire value is the safe-and-idempotent promise; making that promise about a write is a correctness bug that will surface the first time a retry layer double-fires it.

And if you ship to the open internet today, the compatibility math often favors POST /search with a documented, cache-busting convention. POST works through every intermediary right now. QUERY works through the ones that have shipped RFC 10008 support — a set that will grow for years before it is universal. The honest position in mid-2026: QUERY is production-ready for controlled environments (internal service meshes, your own gateway, mobile clients hitting your own API) and premature for uncontrolled public paths where you don't own the middleboxes.

The SEO and Infrastructure Advantage You Actually Gain

The payoff, stated concretely, is that a read finally gets to behave like a read across the whole stack.

Edge caching becomes possible for complex queries for the first time. A POST /search response is uncacheable at any CDN; the same query as QUERY, exposed through its Content-Location GET URL, can sit in a CDN with a max-age and serve p99s in single-digit milliseconds instead of round-tripping to origin. For a search-heavy product, that is the difference between a search box that feels instant and one that spins — the same caching-first instinct I walk through in Next.js Performance Patterns.

Automatic retries stop being dangerous. A dropped connection on POST /search forces your client to choose between a failed search or a risky retry. QUERY's idempotency lets the service mesh retry transparently, which collapses your tail latency under packet loss without any application code.

And the requests stop poisoning your observability. URL-encoded JSON filters turn access logs into noise and leak filter values into every log aggregator, proxy cache, and browser history along the path — a real concern when the filter contains a customer ID or an email. Moving the query into the body keeps it out of the URL, out of the logs, and out of the referrer header. The RFC even calls this out: sensitive query data in the body avoids the URI-logging exposure GET has always had.

Six months into running QUERY on an internal API, the thing you will notice is not the elegance — it is the boring absence of two recurring incidents: the 414 that only your biggest customers hit, and the "why is search not cached" ticket that had no good answer. QUERY doesn't add a capability you couldn't fake before. It removes the lie you had to tell to fake it.

If you're building an API where this distinction matters, I'm always happy to talk shop — take a look at my projects or get in touch.

References


I use AI tools to help research and draft posts. The ideas, opinions, and takes are mine. Verify anything technical or time-sensitive before acting on it.