The HTTP QUERY method (RFC 10008) is here — and caching it correctly is harder than it looks

typescript dev.to

TL;DR

  • HTTP got a new method: QUERY (RFC 10008, June 2026). It's a safe, idempotent, cacheable request that carries a body — "GET with a body."
  • It fixes a decade-old pain: complex reads (search/filter/graph queries) that don't fit in a URL, without abusing POST.
  • The subtle part is caching: the RFC requires the request body to be part of the cache key. Get it wrong and one client gets another client's response.
  • http-queryable is a small library that does this correctly for Express, Fastify, raw http, and the browser.

What is the QUERY method?

For decades HTTP forced a bad choice for "reads with a big input":

  • GET — safe, idempotent, cacheable, but no body. Cram everything into the URL and hit length limits.
  • POST — has a body, but it's unsafe and non-idempotent. Caches and proxies won't treat it as a read, and you can't safely retry.

QUERY is the missing middle: a body like POST, but the semantics of GET — safe, idempotent, and cacheable.

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

{ "q": "cats", "filters": { "color": "black" } }
Enter fullscreen mode Exit fullscreen mode

The catch nobody talks about: caching

RFC 10008 §2.7 is explicit: the cache key must incorporate the request content.

Shared HTTP caches have always keyed on method + URL. With QUERY, many different bodies hit the same URL:

QUERY /search  { "q": "cats" }   -> cats
QUERY /search  { "q": "dogs" }   -> dogs
Enter fullscreen mode Exit fullscreen mode

A method+URL cache would serve the cats response to the **dctness and security bug — the RFC's Security Considerationscalls it out.

Doing it right: conservative body normalization

You want two things in tension:

  1. {"a":1,"b":2} and { "b":2, "a":1 } mean the same thing → same key (a hit).
  2. {"q":"cats"} and {"q":"dogs"} differ → *different keys

The key insight is an asymmetry: a false miss is harmle** is a bug (wrong data served). So every normalization must be*provably meaning-preserving, and when in doubt you normalize *less.

Safe for JSON: whitespace, object key order, string escape forms. NOT safe: merging numeric literals (JSON.parse("9007199254740993") returns ...992 — a
silent collision), guessing on duplicate keys, or normalizing ctand.

Show me the code

import express from "express";
import { queryable, QueryCache } from "http-queryable/express";

const app = express();
app.use(queryable({ cache: new QueryCache() }));
app.query("/search", (req, res) => res.json(search(req.body)));
app.listen(3000);

curl -X QUERY /search -d '{"q":"cats"}'      # MISS -> cats
curl -X QUERY /search -d '{ "q" : "cats" }'  # HIT  (same meaning)
curl -X QUERY /search -d '{"q":"dogs"}'      # MISS -> dogs
Enter fullscreen mode Exit fullscreen mode

That third line is the whole point: a different body gets the cr a stale hit.

It handles the rest too

Accept-Query negotiation, Content-Location "switch to GET" flow CORS-safelisted), an isomorphic client with safe auto-retry,and Fastify + raw http adapters over the same core.

Try it

Node >= 22 (where QUERY lands in http.METHODS).

npm install http-queryable
Enter fullscreen mode Exit fullscreen mode

Repo + docs: https://github.com/hardik-goel/http-queryable — stars and edge-case bug reports welcome.

Source: dev.to

arrow_back Back to Tutorials