We got new HTTP method after 20 years

RFC 10008 introduces the HTTP QUERY method — a safe, idempotent, cacheable request that carries a body. Here's what it fixes, how it works, and how to use it.

We got new HTTP method after 20 years

Think about the last time you needed to search through a list of users, filter products by multiple criteria, or send a complex query to an API. You probably wrote a POST endpoint for it. Not because POST was the right choice — but because GET refused to carry a body, and jamming a JSON filter into a URL is a recipe for disaster.

For twenty years, that’s been the compromise. Pick the semantically wrong method (POST) or fight with URL length limits, encoding issues, and leaking sensitive data through logs (GET). Neither option is good. Both are workarounds for a gap that should never have existed.

In June 2026, the IETF closed that gap. RFC 10008 introduces the QUERY HTTP method — and it changes everything about how we design read-oriented APIs.


The Two Bad Options We’ve Been Stuck With

GET: The Clean Semantics That Can’t Handle Reality

GET is beautiful in theory. It’s safe, idempotent, cacheable, and any intermediary on the internet knows exactly what it means. The resource is fully identified by the URI, so caching just works and retries are always safe.

But GET has a fatal flaw: it can’t carry a body. RFC 9110, the core HTTP semantics spec, explicitly says content received in a GET request “has no generally defined semantics” and “might lead some implementations to reject the request.”

So everything has to go in the URL. That works fine when you’re filtering by a single field. It falls apart fast when you need:

  • Nested filters with complex boolean logic
  • Arrays of values
  • Geo queries with coordinates and radius
  • Full-text search with multiple options

The URL wasn’t designed for payloads. RFC 9110 only recommends servers support at least 8,000 octets — but that’s a floor, not a promise. Your local dev server might accept 16KB URLs. Your corporate proxy might cut off at 4KB. The 414 error you get is always from a system you didn’t test against.

There’s a subtler problem too: URLs leak. Every proxy, CDN, and server along the path logs the full URL. If your query contains an email, a user ID, or any sensitive data, it’s now sitting in log files across your infrastructure. The URL also ends up in browser history, bookmarks, and referrer headers.

And my personal favorite footgun: ?city=Berlin&limit=50 and ?limit=50&city=Berlin are semantically identical, but a naive cache treats them as different resources. That’s a surprisingly easy way to destroy your cache hit rate without realizing it.

POST: The Pragmatic Hack That Became Industry Standard

POST works today. That’s why every codebase has a POST /search or POST /query endpoint. You throw your JSON in the body, the server processes it, and you get results back. No URL size limits. No encoding headaches. It just works.

The cost is subtle but real. POST is not safe. It’s not idempotent. And for most caching layers, it’s not cacheable.

What does that mean in practice? When a POST connection drops mid-request, neither the client nor any intermediary knows whether the request was processed. Maybe it went through. Maybe it didn’t. You can’t safely retry without idempotency keys or deduplication logic — even though you were just reading data.

Caching is worse. RFC 9110 explicitly says “a POST request cannot be satisfied by a cached POST response.” Every dashboard that polls a search endpoint every 30 seconds is hitting your origin server on every single tick. Your users are paying for a full query execution — even when the exact same query was run 10 seconds ago.

POST is a lie. You’re telling the protocol “I might change state” when you’re only asking a question. And the protocol takes you at your word — no caching, no safe retries, no optimizations.


QUERY: The Method HTTP Should Have Always Had

RFC 10008 introduces QUERY as the third option. It’s exactly what the gap needed: a method that combines GET’s safety guarantees with POST’s ability to carry a body.

Feature              GET     QUERY    POST
────────────────────────────────────────────
Safe                 ✅      ✅       ❌
Idempotent           ✅      ✅       ❌
Cacheable            ✅      ✅       ❌
Request body         ❌      ✅       ✅

The key difference isn’t technical — it’s protocol-level. Because QUERY is registered in the IANA HTTP Method Registry as safe and idempotent, every generic HTTP client, proxy, and cache can treat it that way automatically. The guarantee moves from “please read our API docs” to “the protocol enforces this.”

The spec defines it simply: “A QUERY requests that the request target process the enclosed content in a safe and idempotent manner and then respond with the result of that processing.” In plain English: the request body is your question, and the response is the answer. No state changes. No side effects. Just a query and its results.


How QUERY Works in Practice

The Basics

A QUERY request is straightforward:

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

{
  "filter": { "city": "Berlin" },
  "select": ["name", "email"],
  "limit": 50
}

The Content-Type header is mandatory. If you omit it or send something the server doesn’t understand, you get a 415 Unsupported Media Type.

Content Negotiation via Accept-Query

The spec introduces a new response header: Accept-Query. It lets servers advertise which query formats they accept:

HTTP/1.1 200 OK
Accept-Query: "application/jsonpath", application/sql;charset="UTF-8"

Clients can check this with OPTIONS or HEAD before sending a query. No more guessing and getting a 415.

The Equivalent Resource Idea

Here’s where QUERY shows its sophistication. The RFC introduces the concept of an equivalent resource — a conceptual GET-able resource that combines the target URI with the query content.

Servers can expose this through two headers that serve different purposes:

  • Content-Location — Points to a snapshot of the query results. “Here’s what this query returned at this moment.” Useful for caching a specific answer.
  • Location — Points to the query itself as a reusable URI. “GET this URI to re-run the same query.” Turns an expensive QUERY into a cacheable GET on subsequent requests.
HTTP/1.1 200 OK
Content-Type: application/json
Content-Location: /contacts/queries/snapshot/a1b2c3d4
Location: /contacts/queries/saved/x8y9z0

[ ... results ... ]

Get the distinction wrong and your caching layer will serve stale data. Content-Location is a snapshot. Location is a reusable query.

Caching: The Hard Part

Caching a QUERY response is fundamentally harder than caching a GET. The cache key must include the full request body, not just the URI. Section 2.7 is explicit: caches MUST incorporate the request content into the key.

This means a cache has to read the entire body before it can check for a hit. To reduce misses from trivial differences, caches are allowed to normalize — stripping formatting differences, handling equivalent content encodings, and treating +json suffixes uniformly.

The danger: over-normalization can cause false-positive cache hits, returning wrong results for queries that happen to look similar after normalization. If you’re building a QUERY-aware cache, this is the bug to lose sleep over.

Error Handling

The RFC defines a clean error model:

Status CodeMeaning
400 Bad RequestContent-Type matched, but the body is malformed (invalid JSON, etc.)
415 Unsupported Media TypeContent-Type missing or not supported
422 Unprocessable ContentValid format, but the query itself fails (references a non-existent field)
405 Method Not AllowedThis resource doesn’t support QUERY

The Long Road from SEARCH to QUERY

The idea for a query-carrying HTTP method first appeared in a 2015 draft from James Snell, originally called SEARCH. It stayed a draft for years, was revived at the 2019 HTTP Workshop, and got adopted by the httpbis working group in March 2021 — still named SEARCH.

The rename to QUERY happened in November 2021. The reason is buried in Appendix B of the RFC: WebDAV already defined PROPFIND, REPORT, and SEARCH — all safe, idempotent methods with bodies — and people have “mixed feelings” about WebDAV. QUERY got a clean slate.

The name also fits better. A URI has a “query component” (?key=value). QUERY extends that same idea to structured request bodies. It’s not a coincidence.

The timeline: 2015 draft → 2019 workshop revival → 2021 working group adoption → November 2025 final draft → November 2025 IESG approval → June 2026: RFC 10008 published. Eleven years from first draft to standard. HTTP doesn’t move fast, but when it does, it tends to stick.


Code: Making QUERY Work Today

Node.js (Native Support)

Node’s HTTP parser has supported QUERY since llhttp 9.2.0, which shipped in Node 21.7.2 and all Node 22+ releases. Zero configuration needed:

import { createServer } from 'node:http';

const server = createServer((req, res) => {
  if (req.method !== 'QUERY') {
    res.writeHead(405, { Allow: 'QUERY' });
    return res.end();
  }

  let body = '';
  req.on('data', chunk => body += chunk);
  req.on('end', () => {
    const query = JSON.parse(body);
    const results = searchContacts(query);

    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify(results));
  });
});

server.listen(3000);

Go (Arbitrary Method Strings)

Go doesn’t have a http.MethodQuery constant yet, but req.Method is just a string. Go 1.22’s ServeMux handles any method:

package main

import (
  "encoding/json"
  "io"
  "net/http"
)

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("QUERY /contacts", handleQuery)
  mux.HandleFunc("GET /contacts/queries/{id}", handleStoredQuery)
  http.ListenAndServe(":8080", mux)
}

func handleQuery(w http.ResponseWriter, r *http.Request) {
  body, _ := io.ReadAll(r.Body)
  defer r.Body.Close()

  var q struct {
    Filter map[string]string `json:"filter"`
    Limit  int              `json:"limit"`
  }
  json.Unmarshal(body, &q)

  results := runQuery(q)
  json.NewEncoder(w).Encode(results)
}

Client Side (fetch)

const res = await fetch('https://api.example.com/contacts', {
  method: 'QUERY',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    filter: { city: 'Berlin' },
    limit: 50
  })
});

const data = await res.json();

CORS note: QUERY is not a safelisted method. Every cross-origin request triggers an OPTIONS preflight. Plan for that extra round trip.

cURL

curl -X QUERY 'https://api.example.com/contacts' \
  -H 'Content-Type: application/json' \
  -d '{"filter": {"city": "Berlin"}, "limit": 50}'

Framework and Tooling Support (July 2026)

PlatformWhat Works Today
Node.js 22+Native. Parser handles it out of the box.
Fastifyfastify.addHttpMethod('QUERY', { hasBody: true })
Go 1.22+Works via string method names. No constant yet.
ASP.NET Core (.NET 11)Preview 4 recognizes QUERY and emits it in OpenAPI
OpenAPI 3.2First-class query operation field
Spring FrameworkPR #34993 open. Not merged.
ExpressWorks on Node 22+, but no TypeScript types
Browsersfetch() accepts it. Always preflights cross-origin.

The Spring situation is the one to watch. Issue #32975 sat dormant for two years. PR #34993 was marked ready for review the same week RFC 10008 published. As of July 2026, RequestMethod still doesn’t have a QUERY value, so you can’t use @RequestMapping with it. A servlet filter can intercept QUERY methods as a stopgap.


Should You Use QUERY Right Now?

Internal APIs: Yes

If you control both the client and server, there’s no reason to wait. Node 22 handles it natively. The semantics are standardized. Your internal proxies can be configured to support it. Every retry and cache you control starts benefiting immediately.

Public APIs: Adopt Alongside POST

Ship Accept-Query on endpoints that support it. Keep your POST search endpoint for older clients. Document both in OpenAPI 3.2. Let early adopters use QUERY while everyone else stays on the legacy path. When the tooling catches up — generated clients from OpenAPI specs, CDN support, browser maturity — you can deprecate the POST endpoint.

The Rollout Strategy

  1. Implement QUERY on your search/query endpoints
  2. Start returning Accept-Query headers
  3. Let internal services migrate first
  4. Document both methods in your API specs
  5. Deprecate POST search in 6-12 months

The Bottom Line

POST /search has been the industry’s pragmatic lie for two decades. We all knew it was semantically wrong. We all used it anyway because the alternative — cramming complex queries into URLs — was worse.

RFC 10008 finally fixes this. QUERY is safe where POST isn’t. It carries a body where GET can’t. It’s cacheable, idempotent, and protocol-level guaranteed. The method finally matches the meaning.

The fact that it took 11 years and an IETF standard to go from draft to publication tells you how seriously the HTTP community takes method changes. This isn’t a throwaway experiment. It’s a carefully designed, thoroughly reviewed addition to the protocol that will be with us for decades.

POST /search was always a hack. We just didn’t have a choice — until now.


Try this today: Spin up a Node 22 server with a single QUERY handler. curl it from your terminal. The moment you realize you can retry without fear and cache without hacks is the moment POST for reads starts feeling like what it always was: a workaround.

References

Happy querying 🚀

Member discussion

0 comments

Start the conversation

Become a member of >hacksubset_ to start commenting.