Blog Detail

Insights, stories, and updates from the world of technology and innovation.

Blog Featured Image

HTTP Just Got a New Verb: Inside RFC 10008 and the QUERY Method

Published on Sep 2 hours ago · By FlipCode Team


A Hole in HTTP That's Been There Since the Beginning

RFC 10008  ·  HTTP WORKING GROUP  ·  STANDARDS TRACK
GETPOSTPUTPATCHDELETEOPTIONSQUERY

A Hole in HTTP That's Been There Since the Beginning

For twenty years the web's read operations had to choose between a URL that breaks and a method that lies. In June 2026, the IETF finally closed the gap.

~10 MIN READ PROTOCOLS / WEB STANDARDS JS · NODE · REACT EXAMPLES INSIDE

Quick — how many HTTP methods can you name off the top of your head?

Most developers land on the same five or six: GET, POST, PUT, DELETE, maybe PATCH, maybe OPTIONS if they've had to deal with CORS. That's basically been the entire vocabulary of the web for over two decades. New frameworks came and went, REST gave way to GraphQL and back again, JSON replaced XML as the default wire format — but the actual verbs used to talk to a server over HTTP have stayed frozen since the early 2000s.

That changed in June 2026. The IETF's HTTP Working Group published RFC 10008: The HTTP QUERY Method, written by Julian Reschke, James M. Snell (Cloudflare), and Mike Bishop (Akamai). It defines a new method — literally named QUERY — and it's the first general-purpose addition to HTTP's method vocabulary in more than twenty years.

That's a big deal, and also a strange one, because on the surface QUERY doesn't sound like it does anything you couldn't already do. You could already send a body in a POST request. You could already run search endpoints. So why did it take an actual IETF standards process to formalize this? Once you dig into it, the answer says a lot about what's actually broken in how the web handles "read" operations — and it's been broken for a very long time.

This article walks through what QUERY actually is, the exact gap it closes, what's genuinely new in the spec, and how to start using it in real code today — in JavaScript, Node/Express, and React — even while browser support is still catching up.

The Problem: Every API Eventually Needs a Search Endpoint That Doesn't Fit Anywhere

Picture a fairly ordinary API. You've got a /contacts resource. GET /contacts/42 fetches one contact. GET /contacts lists them all. Then, inevitably, someone on the product team asks for a search feature: filter contacts by name, by email domain, by tag, sorted a certain way, paginated, maybe with a few nested "OR this AND that" conditions thrown in.

At that point, every API designer runs into the exact same wall, and reaches for one of two well-worn but flawed solutions.

Attempt 1 — keep it a GET

The first instinct is to keep it a GET, and stuff everything into the query string:

GET /contacts?tag=customer&domain=example.com&sort=-created&limit=25

This is the "correct" answer according to REST purism — GET is safe, it's idempotent, it's cacheable, the resource is fully identified by the URI. For simple filters, it works fine. But push it further — nested filter groups, nested boolean logic, nested arrays, a whole embedded GraphQL query — and this approach falls apart fast:

  • Nobody actually knows how long a URL is allowed to be. A request travels through your load balancer, maybe a CDN, maybe a corporate proxy, before it even reaches your server, and each of those has its own undocumented limit. The relevant HTTP spec only recommends 8,000 octets as a rough floor — everything past that is a gamble.
  • Structured data doesn't survive URL encoding gracefully. A nested JSON filter object turns into an unreadable wall of percent-encoding the moment you push it into a query string. It's technically possible; it's miserable to debug, log, or hand-write.
  • URLs get logged, cached, bookmarked, and stored in browser history — bodies don't. If any part of that search query is remotely sensitive, putting it in a URL is a genuine privacy and security liability.
  • GET, strictly speaking, doesn't support a request body at all. You can attach one, but nothing in the HTTP spec defines what a server should do with it, and in practice implementations disagree wildly — some silently drop it, some reject the request outright. It's a trap, not a workaround.

Attempt 2 — just use POST

So the second instinct — the pragmatic one almost every real-world API eventually settles on — is to just use POST:

POST /contacts/search HTTP/1.1
Content-Type: application/json

{ "tag": "customer", "domain": "example.com", "sort": "-created", "limit": 25 }

This works. It's flexible, the body can be as large and as structured as you want, and every framework and library handles it without complaint. But it comes at a cost that's easy to overlook until it bites you: POST is not safe, and it's not idempotent. Nothing in the protocol tells a cache, a proxy, a retry-on-failure HTTP client, or a browser's back button that this particular POST is actually harmless and repeatable. As far as the infrastructure is concerned, every POST might be charging a credit card or deleting a row, so none of it gets the automatic caching, safe retries, or prefetching that a "real" read operation deserves.

You end up bolting on your own conventions — comments in the code, a line in the API docs — to say "trust me, this one's just a search." Infrastructure can't read comments.

The actual hole

Not a missing feature — a missing promise. There was no method that meant "this carries a real body, and it's still just a read."

Enter QUERY

RFC 10008 closes that gap directly. Here's the plainest way to describe it:

In one line

QUERY is a request that carries a body like POST, but is guaranteed safe and idempotent — the same category as GET.

Here's what it looks like on the wire:

QUERY /contacts HTTP/1.1
Host: example.org
Content-Type: application/x-www-form-urlencoded
Accept: application/json

select=surname,givenname,email&limit=10&match=%22email=*@example.*%22

HTTP/1.1 200 OK
Content-Type: application/json

[
  { "surname": "Smith", "givenname": "John", "email": "smith@example.org" },
  { "surname": "Jones", "givenname": "Sally", "email": "sally.jones@example.com" }
]

Look closely and it's almost indistinguishable from a POST request — same shape, same headers, same idea of sending a structured body. The difference is entirely at the protocol level: QUERY is formally registered with IANA as safe and idempotent, meaning any HTTP-aware piece of infrastructure — a cache, a CDN, a browser, a retrying client — can now know, without reading your API documentation, that this request is read-only and can be retried or cached without any risk of double-charging, double-deleting, or otherwise corrupting state.

That single formal guarantee is the entire point of the spec. Everything else in RFC 10008 is really just working out the consequences of that guarantee.

What's Actually New, Mechanically

1 — A first-class citizen in the method registry

QUERY sits in IANA's HTTP Method Registry alongside GET, HEAD, and OPTIONS as safe and idempotent. That's not a documentation convention — it's the same formal classification that lets browsers safely prefetch a GET link or retry a failed GET without asking the user. QUERY inherits all of that.

MethodSafeIdempotentHas body
GETyesyesundefined
POSTnonoyes
QUERYyesyesyes

2 — Accept-Query: a header for discovering what a server accepts

One underrated detail in the spec: a resource can support more than one query "language." Maybe /contacts understands simple form-encoded filters and a more expressive SQL-like syntax and JSONPath. Servers can advertise all of them:

HTTP/1.1 200 OK
Accept-Query: "application/x-www-form-urlencoded", application/sql, "application/jsonpath"

A client can discover this ahead of time with a plain OPTIONS or HEAD request, before ever committing to a specific query format — genuinely useful for API clients, SDKs, and tooling that want to adapt to whatever a given server supports.

3 — Query results can get their own URLs

Two response headers matter a lot here:

  • Content-Location points to a URL for this exact result set — useful if you want to let someone share a link to "the search results I just got," even though the original request was a body-bearing QUERY, not a bookmarkable GET.
  • Location, paired with a 303 See Other, points to a URL that will re-run the same query later — letting a server hand off from "run this expensive structured query once" to "serve it cheaply from cache via GET from now on."

This preserves one of the oldest principles in REST — anything meaningful deserves a URI — even for a query that's too complex to fit in one to begin with.

4 — Real caching, not just a promise of caching

Because QUERY is formally safe and idempotent, it gets full support for conditional requests: ETag, If-None-Match, If-Modified-Since, 304 Not Modified — all of it, exactly as GET has always had. The one wrinkle is the cache key: since two QUERY requests to the same URL can carry completely different bodies (and therefore mean completely different things), a spec-compliant cache has to fold the request body — and relevant headers — into the cache key itself, not just the path. Get that wrong, and you open the door to cache poisoning, where one user's cached query result leaks to another user who asked something entirely different.

The Catch Nobody's Pretending Isn't There

Here's the honest part of the story. The RFC is finished and published — Standards Track, final, not going to change. But the actual internet is still mid-migration, and that shows up in a few concrete ways:

  • Browsers don't fully support sending QUERY natively yet. fetch() and XMLHttpRequest weren't built with this method in mind, and native support is expected to roll out gradually through 2026 and into 2027 as browser vendors catch up.
  • QUERY isn't CORS-safelisted. Any cross-origin QUERY request triggers a preflight OPTIONS check, exactly like PUT, DELETE, and PATCH already do.
  • Frameworks are at different stages. Express can already handle it with a little manual routing (shown below). Other ecosystems are actively working on native support but aren't there yet across the board.
  • Method allowlists everywhere — WAFs, API gateways, CDNs, corporate firewalls — were written with only GET/POST/PUT/DELETE/PATCH in mind. A method allowlist configured before mid-2026 has never heard of QUERY, and depending on the vendor, it might silently drop the request, reject it outright, or route it somewhere unexpected.

None of that means QUERY isn't usable today. It means today's usage looks like a bridge: real server-side support, paired with a client-side fallback that degrades gracefully until the rest of the stack catches up.

Trying It Yourself: Server Side (Node.js / Express)

Express doesn't ship an app.query() helper yet, but nothing stops you from handling the method manually:

// server.js
import express from "express";

const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());

// Express normalizes methods internally, so we intercept at the
// raw request level to catch "QUERY" before routing kicks in
app.use((req, res, next) => {
  if (req.method === "QUERY") {
    return handleContactQuery(req, res);
  }
  next();
});

function handleContactQuery(req, res) {
  const { select, limit, match } = req.body;

  // Stand-in for a real data store lookup
  const results = db.contacts.search({ select, limit, match });
  const resultId = db.savedResults.store(results);

  res
    .status(200)
    .set("Content-Type", "application/json")
    .set("Content-Location", `/contacts/results/${resultId}`)
    .json(results);
}

app.listen(3000, () => console.log("Listening on :3000"));

Advertise the capability so well-behaved clients can discover it ahead of time:

app.options("/contacts", (req, res) => {
  res.set("Allow", "GET, QUERY, OPTIONS, HEAD").sendStatus(200);
});

app.head("/contacts", (req, res) => {
  res
    .set("Accept-Query", '"application/x-www-form-urlencoded", application/sql')
    .sendStatus(200);
});

Trying It Yourself: Client Side (Plain JavaScript)

Since native fetch() support for the literal QUERY verb is still rolling out across browsers, the practical pattern today is a thin wrapper: attempt QUERY, and fall back to a POST with an explicit override header if the environment doesn't cooperate.

// httpQuery.js
export async function httpQuery(url, { body, headers = {}, ...rest } = {}) {
  const requestInit = {
    method: "QUERY",
    headers: { "Content-Type": "application/x-www-form-urlencoded", ...headers },
    body,
    ...rest,
  };

  try {
    const res = await fetch(url, requestInit);
    // Some intermediaries reject an unrecognized method outright
    if (res.status === 405 || res.status === 501) {
      throw new Error("QUERY not supported by this endpoint");
    }
    return res;
  } catch {
    // Transitional fallback while browser/infra support catches up
    return fetch(url, {
      ...requestInit,
      method: "POST",
      headers: { ...requestInit.headers, "X-HTTP-Method-Override": "QUERY" },
    });
  }
}

Using it is exactly as simple as a normal fetch call:

const res = await httpQuery("https://example.org/contacts", {
  body: new URLSearchParams({
    select: "surname,givenname,email",
    limit: "10",
    match: "email=*@example.*",
  }),
  headers: { Accept: "application/json" },
});

const contacts = await res.json();
console.log(contacts);

And you can discover a server's supported query formats before committing to one:

async function discoverQueryFormats(url) {
  const res = await fetch(url, { method: "HEAD" });
  const acceptQuery = res.headers.get("Accept-Query");
  return acceptQuery ? acceptQuery.split(",").map((s) => s.trim()) : [];
}

Trying It Yourself: React

Wrapping the fetch helper in a small hook makes this genuinely pleasant to use from a component — especially for a "search with real filters" UI where a GET query string would already be getting unwieldy.

// useHttpQuery.js
import { useState, useCallback } from "react";
import { httpQuery } from "./httpQuery";

export function useHttpQuery(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const run = useCallback(
    async (params) => {
      setLoading(true);
      setError(null);
      try {
        const res = await httpQuery(url, {
          body: new URLSearchParams(params),
          headers: { Accept: "application/json" },
        });
        if (!res.ok) throw new Error(`Request failed: ${res.status}`);
        setData(await res.json());
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    },
    [url]
  );

  return { data, loading, error, run };
}
// ContactSearch.jsx
import { useHttpQuery } from "./useHttpQuery";

export default function ContactSearch() {
  const { data, loading, error, run } = useHttpQuery("https://example.org/contacts");

  const handleSubmit = (e) => {
    e.preventDefault();
    const form = new FormData(e.target);
    run({
      select: "surname,givenname,email",
      limit: form.get("limit") || "10",
      match: form.get("match") || "",
    });
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input name="match" placeholder="e.g. email=*@example.*" />
        <input name="limit" type="number" placeholder="Limit" />
        <button type="submit">Search</button>
      </form>

      {loading && <p>Loading…</p>}
      {error && <p style={{ color: "red" }}>{error}</p>}

      <ul>
        {data?.map((c, i) => (
          <li key={i}>
            {c.givenname} {c.surname} — {c.email}
          </li>
        ))}
      </ul>
    </div>
  );
}

Notice that everything QUERY-specific lives inside httpQuery.js — the React component itself doesn't know or care that anything unusual is happening under the hood. That's deliberate. The same helper drops cleanly into Vue, Svelte, Angular, or plain vanilla JS without any changes, because the interesting part of this story is a protocol-level change, not a framework-level one.

Should You Actually Adopt This Now?

If you're weighing whether to start migrating, here's a realistic way to think about it:

  • Good candidates for a QUERY endpoint today: any POST /search, POST /reports, or GraphQL-style read endpoint that only ever reads data and never mutates anything. These are exactly the endpoints that have been quietly lying to your infrastructure by pretending to be writes.
  • Worth double-checking before you ship it: your WAF and API gateway's method allowlist, your CDN's cache-key configuration, and any CSRF middleware — all of these were very likely written with only GET/POST/PUT/DELETE/PATCH in mind, and an unrecognized method can behave unpredictably depending on the vendor.
  • Not fully there yet: native browser fetch() support. Build the POST-with-override fallback shown above as a deliberately temporary shim, and plan to delete it once browser support solidifies.
Security note

QUERY isn't CORS-safelisted, so cross-origin requests will trigger a preflight, same as PUT/PATCH/DELETE already do. And if you're assigning a Location or Content-Location URL to a query's results, don't undo the whole point of switching away from GET by encoding the sensitive parts of the query back into that URL.

The Bigger Picture

It's easy to read all of this and think "fine, it's a POST that pinky-promises not to break anything" — but that undersells what actually changed. The interesting part isn't that you can now send a body on a safe request; you could always fake that with a POST and a comment in your API docs. The interesting part is that the promise is now legible to machines, not just to humans reading documentation.

A cache doesn't read your API docs. A retrying HTTP client doesn't know your internal conventions. A CDN edge node has never seen your codebase. QUERY gives all of that infrastructure a way to know, from the method name alone, that a request is safe to retry, safe to cache, and safe to prefetch — the exact same guarantee GET has quietly provided since HTTP/1.0, now available for the messy, structured, oversized queries that GET was never built to carry.

Twenty years is a long time to wait for one new verb. Given how much of the modern web runs on search-shaped POST requests pretending to be writes, it's a reasonable bet that QUERY becomes just as unremarkable and ubiquitous as GET and POST already are — it's just going to take the browsers, the frameworks, and the infrastructure a couple of years to get there.

RFC 10008 · THE HTTP QUERY METHOD · J. RESCHKE, J. SNELL (CLOUDFLARE), M. BISHOP (AKAMAI) · IETF HTTP WORKING GROUP