DEV Community

Cover image for HTTP finally shipped QUERY. I fired it at production and the edge called it a bot.
Vadym Arnaut
Vadym Arnaut

Posted on

HTTP finally shipped QUERY. I fired it at production and the edge called it a bot.

TL;DR. QUERY (RFC 10008) is a real HTTP method now, a GET that carries a body. I deployed echo endpoints on Vercel (Python and Node), Supabase Edge, and local FastAPI, then fired GET, POST and QUERY at each. Every runtime handled QUERY fine. Then Vercel's default bot mitigation started 403-challenging my QUERY traffic specifically. Same client, same body: 20 GETs clean, 20 POSTs clean, QUERY tripped the challenge on request #4. Reproduced from a second client. The functions are ready for QUERY. The edge in front of them is not.

RFC 10008 landed in June. If you missed the wave of explainers: QUERY is a request method with GET semantics (safe, idempotent, cacheable) that carries its query in the body instead of the URL. It closes the old gap where a search filter is too big for a query string but you don't want to lie and call it a POST.

Every post I read explained the spec. Nobody actually fired it at a live production edge. So I did, late one night, with a throwaway lab. Most of it was boring. One result made me sit up and say "wait, what?" out loud. Repo at the bottom.

The setup

Four echo endpoints, each returns the method and body it saw:

  • Vercel serverless, Python runtime
  • Vercel serverless, Node runtime
  • Supabase Edge Function (Kong gateway in front of Deno)
  • local FastAPI on uvicorn, for the parser tests

The whole Python function is basically this:

# api/echo.py on Vercel
class handler(BaseHTTPRequestHandler):
    def do_QUERY(self):   # yes, you just define do_QUERY
        self._echo()
    def do_GET(self):
        self._echo()
Enter fullscreen mode Exit fullscreen mode

Python's BaseHTTPRequestHandler dispatches on do_<METHOD>, so do_QUERY is all it takes. Miss the handler and you get a clean 501, which is exactly what the RFC wants.

The runtimes are ready

This part was boring in the best way. QUERY went end to end everywhere.

curl -X QUERY https://http-query-lab.vercel.app/api/echo -d '{"q":"hi"}'
# {"runtime":"python","method":"QUERY","body":"{\"q\":\"hi\"}"}
Enter fullscreen mode Exit fullscreen mode

Node's parser (llhttp) has known QUERY since v21.7.2, it sits in http.METHODS on 22 and 26. Supabase's Deno gateway forwarded it on both HTTP/1.1 and HTTP/2. The only 405 I got was on a static asset, which is correct.

Frameworks are where it gets uneven. What actually happens today:

Layer QUERY today
Node http / llhttp in http.METHODS since 21.7.2
Express 5 routes it, app.query() exists (auto-generated from http.METHODS)
Fastify 5 refuses it until addHttpMethod('QUERY', {hasBody:true})
Python http.server define do_QUERY, missing handler gives 501
FastAPI @app.api_route(methods=["QUERY"])
.NET 10 HttpMethod.Query on client and server

Fastify was the one that bit me. A QUERY route throws at registration with "QUERY method is not supported", and an incoming QUERY 404s until you opt in. Not a bug, it validates against a fixed method list. Just a footgun if you assume parity with Express.

Request-path diagram. GET and POST pass through Vercel's edge to the Python and Node functions and return 200. QUERY is stopped at Vercel's edge with a 403 challenge and never reaches the function. Supabase's Kong plus Deno edge forwards QUERY over HTTP/1.1 and HTTP/2 and returns 200.

Every runtime I tried returns QUERY. The only thing that drops it sits at one edge.

Then the edge called it a bot

Here is the part nobody wrote about.

I ran a burst test against the Vercel endpoint. Twenty requests, same client, 400ms apart, one method at a time from the same connection pool. GET first, then POST, then QUERY:

GET   x20:  all 200
POST  x20:  all 200        (POST carries a body too, remember)
QUERY x20:  200,200,200,403,403,403,403...
Enter fullscreen mode Exit fullscreen mode

Four rows of twenty status cells. GET from node runs all twenty green (200 OK). POST from node runs all twenty green. QUERY from node turns red (403 challenge) from the fourth cell on. QUERY from curl turns red from the fifth cell on.

Same client and same body across the top three rows. GET and POST finish clean, QUERY does not.

Request #4 came back 403. X-Vercel-Mitigated: challenge. That 403 was not from my function. It was the edge, and my request NEVER reached the runtime. Once it flagged me, every method got challenged, GET included, for more than ten minutes.

My first thought was the boring one: "it's the client, curl and undici look like bots, of course the scorer hates me." So I controlled for it. The GET and POST bursts above are from the exact same client, the same fingerprint, and POST even carries a body. They ran clean to twenty. Only the method changed.

Then I ran the QUERY burst from a second client, curl instead of undici. Challenged on request #5. Same story!

So it is not the fingerprint, not the body, not the request rate. Twenty GETs and twenty POSTs at the same cadence sail right through. Swap the method to QUERY, change nothing else, and a bot challenge fires in four requests. That is when it clicked: the edge is treating the QUERY verb ITSELF as a bot signal. No custom firewall rules, no attack mode. This is the default behavior on a fresh project.

And honestly? It makes a grim kind of sense. Real browser traffic doesn't send QUERY yet, so to a heuristic "QUERY from a non-browser" looks exactly like a scanner poking your API with a weird verb. The spec shipped faster than the security tooling learned the verb exists. The runtime rolled out the carpet, and the bouncer at the door never got the memo.

You can turn Vercel's bot management off or bypass it with a token, so this isn't fatal. But flip QUERY on for a public endpoint today, sit behind a CDN with default bot rules, and some slice of your legitimate non-browser callers get challenged into a wall. You won't even see it in your app logs, because it never reaches your app.

Two more footguns before you ship QUERY

Lowercase kills it. fetch only uppercases the six classic methods, so fetch(url, {method:'query'}) goes on the wire literally as lowercase query. That is not cosmetic. The default uvicorn parser (httptools, which wraps llhttp) rejects it with a 400 before your app runs:

uvicorn --http httptools:   QUERY -> 200,  query -> 400,  get -> 400
uvicorn --http h11:         QUERY -> 200,  query -> 200,  get -> 200
Enter fullscreen mode Exit fullscreen mode

llhttp matches its method table case-sensitively, so any lowercase verb is an invalid token to it, not just QUERY. Switch to the pure-Python h11 parser and the same lowercase request passes through to a 405. So "my QUERY works locally but 400s in prod" can just be two different parsers. The normalization gap itself is tracked in whatwg/fetch#1938, worth a read.

Nothing caches it yet. The whole selling point of QUERY over POST is that it is cacheable. In practice no browser cache stores it today. I put Cache-Control: public, max-age=300 on a QUERY response against a local origin, no CDN in the path, and fired two identical requests. Both hit the origin. The GET control on the same setup served the second from cache. That matches what jeswr measured for Chrome and Firefox in that same fetch issue. The RFC allows caching QUERY with a cache key that includes the body, but existing caches key on method plus URL, so they structurally can't, and they don't. The "cacheable" promise is real on paper and ZERO in your browser right now.

What I'd want to hear back

This was one edge, one night, one very confused engineer staring at a wall of 403s. I really want to know how far it generalizes.

  • If you're on Cloudflare or AWS WAF, does default bot protection challenge QUERY the way Vercel's does? I only tested one edge, curious whether this is industry-wide or a Vercel default.
  • Anyone running QUERY on a real public endpoint, not a demo? What broke?
  • Is body-aware cache keying ever landing in a shipping cache, or does "cacheable QUERY" stay theoretical?

Lab, all the scripts, and the raw numbers:

GitHub logo ArVaViT / http-query-lab

Empirical tests of the new HTTP QUERY method (RFC 10008) against real clients, frameworks, and production infra

http-query-lab

Empirical tests of the new HTTP QUERY method (RFC 10008, June 2026) against real clients, servers, frameworks, and production infrastructure. Not what the spec says, but what actually happens on the wire in July 2026.

QUERY is a request method with GET semantics (safe, idempotent, cacheable per RFC 10008 §2 and §2.7) that carries its query in the body instead of the URI This repo answers: who on a real request path already speaks it, and who breaks?

Headline: Vercel's default bot mitigation treats QUERY as a bot signal

Twenty requests, one client, 400ms apart, one method at a time:

GET   x20  from node/undici : all 200            (clean)
POST  x20  from node/undici : all 200            (clean, and POST carries a body)
QUERY x20  from node/undici : 200,200,200,403... (challenge at request #4)
QUERY x20  from curl        : 200,...,403...      (challenge at request #5)

The 403 is X-Vercel-Mitigated: challenge from…

Top comments (0)