Meet HTTP QUERY: The Newest HTTP Method You Probably Haven’t Used Yet

How a brand-new, safe-but-body-carrying HTTP method finally fixes the awkward POST /search hack every backend developer has written at least once.

Introduction

Quick question: how many times have you built a POST /search endpoint that doesn’t actually change anything?

If you’ve been writing backends for more than a year, the honest answer is probably “more than I’d like to admit.” You needed to send a complicated filter object to the server, GET couldn’t carry it, so you reached for POST — even though the operation was purely read-only. It worked. It also quietly lied to every cache, proxy, and monitoring tool in the request path about what your endpoint really does.

HTTP methods matter because they are promises. GET promises “I’m just reading.” DELETE promises “something is going away.” Caches, CDNs, load balancers, retry logic, and security tooling all make decisions based on these promises. When you use the wrong method, you break the contract — and the whole ecosystem around your API starts guessing.

In June 2026, the IETF published RFC 10008: The HTTP QUERY Method. It’s an Internet Standards Track document — the same standing as HTTP/1.1 itself — and it defines a genuinely new HTTP method called QUERY. It’s the first new method to be standardized since PATCH back in 2010. QUERY is designed for exactly the situation above: a safe, idempotent, cacheable request that carries its input in the request body instead of cramming it into the URL.

So why haven’t you heard much about it? Because it’s new. The standard is settled, but the tools around it — frameworks, servlet containers, browsers, gateways — are still catching up. That gap between “the spec exists” and “my framework has a @QueryMapping” is exactly what this article is about. We’ll cover what QUERY actually is, what the RFC really says, and — honestly — what you can and can’t use today.

Quick Refresher: The HTTP Methods You Already Know

Before the new arrival, let’s make sure we’re all on the same page about the classics. Think of HTTP methods as verbs — they describe the action you want to take on a resource (a “noun” identified by a URL).

  • GET — “Give me a copy of this.” Retrieval only. It’s safe (it shouldn’t change anything) and idempotent (calling it once or a hundred times has the same effect). Because of this, GET responses are the bread and butter of HTTP caching.
  • POST — “Here’s some data; do something with it.” The general-purpose workhorse. Typically used to create resources or trigger processing. It’s neither guaranteed safe nor idempotent — send it twice and you might create two orders.
  • PUT — “Store this representation at this exact URL.” Used to create or fully replace a resource at a known location. It’s idempotent: PUT the same thing twice and you end up in the same state.
  • PATCH — “Apply this partial update.” Like PUT, but you send only the fields that changed rather than the whole resource. Not guaranteed idempotent.
  • DELETE — “Remove this resource.” Idempotent — deleting something that’s already gone still leaves it gone.

Here’s the quick mental model: use GET to read, POST to create or run actions, PUT to replace, PATCH to tweak, and DELETE to remove. Simple enough — until you need to read something using data too big to fit in a URL. That’s where the cracks appear.

The Problem Before QUERY

GET is the natural choice for reading data. For years, we expressed searches as query strings:

GET /products?category=laptop&brand=dell&minPrice=500&maxPrice=1500&inStock=true

That’s fine for a handful of parameters. But modern search and filtering endpoints aren’t simple anymore. Users want faceted filters, nested boolean logic, ranges, sort orders, geo boxes, and pagination — sometimes across dozens of fields. Try to encode that into a URL and you run into a wall of problems:

  • URL length limits. RFC 9110 recommends that senders and recipients support request targets of at least 8,000 octets, but that’s a floor, not a guarantee. A request can pass through many uncoordinated systems — browsers, proxies, gateways, WAFs — and any one of them may impose a tighter, undocumented limit. You often don’t find out until something silently truncates or rejects your request in production.
  • Complex, nested filters. Query strings are flat key-value pairs. Expressing a nested structure like filters.price.between = [500, 1500] AND (brand IN [dell, lenovo]) turns into an unreadable, bracket-and-encode nightmare.
  • Sensitive data in URLs. URLs get logged — in access logs, proxy logs, browser history, and bookmarks — far more readily than request bodies do. Putting a customer identifier or an auth-ish token in a query string is a genuine privacy and security concern. The RFC explicitly calls this out as a reason to prefer keeping query data in the body.
  • Difficult encoding. Every special character has to be percent-encoded. Complex queries become long, brittle, error-prone strings that are painful to build and debug.

So developers did the pragmatic thing: they moved the query into a request body and used POST.

POST /products/search
Content-Type: application/json
{
"category": "laptop",
"brand": ["dell", "lenovo"],
"price": { "min": 500, "max": 1500 },
"inStock": true
}

This solves the encoding and size problems beautifully. But it introduces a semantic problem: POST is not safe and not idempotent. Nothing in the protocol signals that this particular POST is actually a harmless, repeatable read. So:

  • Caches won’t reuse the response the way they would for a read, because POST responses generally aren’t cacheable for subsequent requests to the same URL.
  • Automatic retries become risky. A generic retry layer can’t safely re-send a POST after a timeout — for all it knows, it might duplicate a side effect.
  • Observability lies. Your dashboards count these as writes. Anyone reading the API sees POST /search and has to know, out of band, that it’s read-only.

You’ve been using a “write” verb to describe a “read.” It works, but it’s semantically wrong — and everyone downstream pays a small tax for the ambiguity.

Introducing QUERY

QUERY is the method that closes this gap. It takes the body support from POST and the safe, idempotent semantics from GET.

Here’s the same search expressed with QUERY:

QUERY /products HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json
{
"category": "laptop",
"brand": ["dell", "lenovo"],
"price": { "min": 500, "max": 1500 },
"inStock": true
}

Notice it targets /products directly — not /products/search. There’s no need for a “verb noun” like /search in the path anymore, because the method already says “this is a query.”

Let’s unpack the concepts the RFC defines:

  • What QUERY is. A method to ask a resource to perform a query described by the request content, and return the result. The content and its media type define the query; the target URI scopes what’s being queried.
  • A safe method. The client isn’t requesting or expecting any change to the server’s state. Like GET, it’s a pure read as far as the target resource is concerned. (The server may create helper resources to hold results — more on that below — but that’s not a change the client asked for.)
  • Idempotent. Repeating a QUERY produces the same effect as sending it once, so it can be safely retried after a dropped connection.
  • Request body support. The query input lives in the request content, not the URL. Crucially, the RFC says the server MUST fail the request if the Content-Type is missing or inconsistent with the body — the media type is mandatory, because it’s what gives the body meaning.
  • Cacheable — with a twist. QUERY responses can be cached. But the cache key MUST incorporate the request body (and related metadata), not just the URL. This is the single most important operational detail to internalize, because a cache that ignores the body would happily serve the wrong results.
  • Content negotiation. QUERY supports the usual machinery: Accept to ask for a response format, plus a dedicated Accept-Query response header so a resource can advertise which query formats it accepts (for example application/sql, application/jsonpath, or application/x-www-form-urlencoded).

The RFC even provides ways to bridge back to GET. A successful QUERY response can include:

  • Content-Location — a URL where the client can GET these specific results again.
  • Location — a URL where the client can GET to re-run the same query later without resending the body.

This is genuinely elegant: you get body-carried queries and a path back into the plain, cache-friendly, bookmarkable GET world.

Practical Examples

Let’s walk through the same conceptual “find products” operation three ways.

Example 1 — Simple GET

Perfect when the filter is small and non-sensitive:

GET /products?category=laptop&inStock=true HTTP/1.1
Host: api.example.com
Accept: application/json

Nothing wrong here. If your filters genuinely fit in a URL, GET is still the right answer — the RFC itself notes that for short queries you’re better off with GET.

Example 2 — POST Search

POST /products/search HTTP/1.1
Host: api.example.com
Content-Type: application/json
{
"category": "laptop",
"brand": ["dell", "lenovo"],
"price": { "min": 500, "max": 1500 },
"specs": { "ram": ">=16GB", "storage": "SSD" },
"sort": [{ "field": "price", "order": "asc" }],
"page": 1,
"pageSize": 20
}

This handles the complex payload, but it’s a read masquerading as a write. Caches skip it, retries are risky, and the /search suffix is a smell that the method couldn’t express intent on its own.

Example 3 — QUERY Request

QUERY /products HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json
{
"category": "laptop",
"brand": ["dell", "lenovo"],
"price": { "min": 500, "max": 1500 },
"specs": { "ram": ">=16GB", "storage": "SSD" },
"sort": [{ "field": "price", "order": "asc" }],
"page": 1,
"pageSize": 20
}

Same payload, but now the semantics are honest. Infrastructure that understands QUERY knows this is safe, idempotent, and cacheable, and it can act accordingly. The endpoint is just /products — the method carries the meaning.

Java HttpClient Example

Good news: you can send a QUERY request with Java’s built-in HttpClient today, no libraries required. Here’s a complete example using Java 21:

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
public class QueryExample {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
String queryBody = """
{
"category": "laptop",
"brand": ["dell", "lenovo"],
"price": { "min": 500, "max": 1500 },
"page": 1,
"pageSize": 20
}
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/products"))
.header("Content-Type", "application/json")
.header("Accept", "application/json")
// No dedicated .QUERY() builder — use the generic method(...)
.method("QUERY", BodyPublishers.ofString(queryBody))
.build();
HttpResponse<String> response =
client.send(request, BodyHandlers.ofString());
System.out.println("Status: " + response.statusCode());
System.out.println("Body: " + response.body());
}
}

Why is there no dedicated .QUERY() method?

HttpRequest.Builder ships convenience methods only for the long-established verbs: .GET(), .POST(...), .PUT(...), and .DELETE(). That API was designed back in Java 11 (2018), years before QUERY existed.

But the builder also exposes a generic escape hatch:

public HttpRequest.Builder method(String method, BodyPublisher bodyPublisher)

This accepts any valid HTTP method token as a string. So QUERY — and any future method — works without waiting for a new JDK release to add a helper. There’s simply no need for a .QUERY() shortcut when .method("QUERY", ...) already does the job cleanly. (The convenience methods are just thin wrappers over method(...) anyway.)

Advantages

Why bother, once the ecosystem is ready?

  • Better semantics. Reads are finally declared as reads. Caches, proxies, retry layers, and dashboards can all trust the method again. POST /search stops being a lie.
  • Large request payloads. Complex, deeply-nested filter objects live comfortably in the body — no URL length anxiety, no percent-encoding gymnastics.
  • Cleaner APIs. Endpoints become nouns again. QUERY /products instead of POST /products/search. Your URL structure describes resources, and the method describes the action.
  • Better than POST for searching. You keep the payload flexibility of POST while regaining the safety, idempotency, and cacheability of GET — including automatic retries and a defined path back to GET via Location/Content-Location.
  • Privacy. Sensitive filter values stay out of URLs, logs, and browser history.

Limitations

Being fair about the downsides:

  • Limited ecosystem support. This is the big one. A perfect method that half your stack doesn’t recognize is a liability, not an asset.
  • Tooling gaps. Not every API client, documentation generator, code generator, or testing tool understands QUERY yet. Expect rough edges.
  • Firewalls and WAFs. Security appliances with method allowlists may reject QUERY outright, or — worse — inspect and route it inconsistently across layers.
  • Infrastructure compatibility. CDNs and caches that don’t incorporate the request body into the cache key can return wrong results or open the door to cache poisoning. Caching a body-keyed method correctly is genuinely harder than caching GET.
  • Framework support. As covered, Spring, ASP.NET, and others are mid-migration. You may be writing bridge code for a while.

Conclusion

For years we’ve had two imperfect options for complex reads: GET, which is semantically correct but can’t carry a body, and POST, which carries a body but lies about being safe. QUERY, standardized as RFC 10008 in June 2026, is the missing third option — safe and idempotent like GET, body-carrying like POST, cacheable when done right, and honest about its intent.

The catch is timing. The standard is finished; the ecosystem isn’t. Java’s HttpClient can send QUERY today, but Spring’s native support is still an open PR targeting version 7.1, browsers need preflights, and plenty of infrastructure predates the method. That makes QUERY an excellent choice for internal, controlled APIs right now — and a “watch this space, keep a fallback” choice for public ones.

Learn it now. Experiment with it on backend-to-backend calls. When your framework ships support, you’ll already know exactly where it belongs.

Key Takeaways

  • QUERY is a real, standardized HTTP method — RFC 10008, IETF Standards Track, published June 2026. It’s the first new method since PATCH in 2010.
  • It combines the best of both worlds: safe and idempotent like GET, body-carrying like POST, and cacheable — the cache key must include the request body.
  • It fixes the POST /search anti-pattern, giving read-only operations honest semantics that caches, proxies, retries, and monitoring can trust.
  • Content-Type is mandatory, and responses can hand you Location/Content-Location URLs to bridge back to plain GET.
  • Java HttpClient supports it today via .method("QUERY", ...); there’s no dedicated builder because the generic one already handles any method.
  • Spring has no native support yet — routing goes through a RequestMethod enum with no QUERY constant. Native support is an open PR targeting Spring Framework 7.1. Use a servlet Filter as a bridge for now.
  • Ecosystem support is real but uneven: Node.js and OpenAPI 3.2 are ahead; browsers need preflights; many gateways, WAFs, and proxies still run pre-2026 method allowlists. Verify every hop in your request path.
  • Use it today for internal/controlled APIs; keep GET for simple reads and a POST fallback for public browser-facing endpoints — and expect the framework matrix to fill in through 2026–2027.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.