DEV Community

Cover image for HTTP QUERY Is Finally Here: Here's Why It Matters More Than You Think
Giorgi Kobaidze
Giorgi Kobaidze Subscriber

Posted on

HTTP QUERY Is Finally Here: Here's Why It Matters More Than You Think

Table of Contents


Introduction

I've been waiting for this for a long time. Not because HTTP is broken, but because there has always been a subtle but awkward gap in the protocol. A gap that developers have been working around for years, usually by stretching GET a little too far or by reaching for POST even though its semantics never quite fit read-only queries.

Every time I had to squeeze one more filter into a GET request or use POST for something that was obviously just fetching data, a voice in my head kept saying:

"There has to be a better way."

And boy, do we have a better way now... finally!

HTTP QUERY fills that gap.

And this is no longer just an interesting proposal floating around the internet. In June 2026, it was officially published as RFC 10008, an IETF Standards Track document.

Before we get into all of that, let's talk about why this article is worth your precious time.

This isn't going to be one of those articles that simply says, "Here's a new HTTP method," and calls it a day. If we're going to talk about QUERY, we first need to understand why it exists at all. That means taking a step back, looking at the history of HTTP, understanding the semantics behind its methods, and seeing how we even got here.

By the end of this article, you won't just know what QUERY is, you'll understand why adding a brand-new HTTP method was considered a better solution than continuing with the workarounds we've been using for decades.


The Long Road From GET to QUERY

HTTP/0.9

To understand why a new HTTP method like QUERY matters, we first need to understand how HTTP methods came to exist in the first place.

HTTP (Hypertext Transfer Protocol) was created in the early 1990s by Tim Berners-Lee while working at CERN. The original goal was simple: allow computers to exchange documents over the newly created World Wide Web.

The earliest version of HTTP, later known as HTTP/0.9, was extremely minimal and simple. It supported only one operation: retrieving a document.

A client would send a request like:

GET /index.html
Enter fullscreen mode Exit fullscreen mode

The server would respond with the contents of that file. There were no headers, no status codes, and no concept of different operations. The web at that time was essentially a collection of linked documents, so this simple model was enough.

Those were simpler times, weren't they? I wasn't even born back then, so I can only imagine.

But as the web grew, it became much more than a document-sharing system. Websites became applications. Users needed to submit forms, upload data, modify resources, and interact with servers in more complex ways.

HTTP evolved to support these new requirements.

HTTP/1.0

With HTTP/1.0, documented in RFC 1945 in 1996, the protocol introduced a richer request and response model, including status codes, headers, and two new methods alongside GET: HEAD and POST. Interestingly, PUT and DELETE appeared only in an appendix of RFC 1945 as "Additional Request Methods", they weren't part of the core specification yet.

HTTP/1.1

Then HTTP/1.1, standardized through RFC 2068 (1997) and RFC 2616 (1999), formally defined PUT, DELETE, OPTIONS, and TRACE, refined the semantics of all methods, and established principles that still define HTTP today. PATCH arrived later through RFC 5789 in 2010. These core method definitions were later carried forward and polished in RFC 7231 (2014) and consolidated in RFC 9110 "HTTP Semantics" (2022), which remains the authoritative reference for general HTTP semantics.


The HTTP Semantics That Absolutely Matter

If you've ever worked with web development, whether on the back end or the front end, this should already be familiar territory.

However, this article is for everyone, not just web developers. So let's quickly recap the fundamentals.

HTTP Method The Official "RFC-Speak" Version The "Explain It Like I'm Human" Version
GET Retrieve a representation of a resource. "Hey server, can I have this thing?"
POST Submit data to be processed by the target resource. "Here is some data. Process it according to this resource's rules."
PUT Create or replace a resource at the target URI. "Here is the complete version of this thing. Save it exactly here."
DELETE Remove the resource identified by the target URI. "This thing is no longer needed. Remove it."
HEAD Same as GET, but the server must not return response content. "Tell me about this thing, but don't send me the actual thing."
OPTIONS Describe the communication options available for the target resource. "Hey server, what can I do here?"
PATCH Apply partial modifications to a resource. "I don't need to replace everything, just change this small part."

The important idea is that HTTP methods are not just shortcuts for database operations. They define semantics: what a client is asking the server to do and what guarantees intermediaries can rely on.

Safety and Idempotence: The Foundations of HTTP Semantics

Safety and idempotence are two concepts that should always be taken into consideration when designing APIs. I cannot stress enough how important they are.

API design is not only about making a request succeed or fail. It is about making sure everyone involved: clients, servers, proxies, caches, and automated systems can predict what happens when that request is sent, repeated, or retried.

Safety

Safety means that the client neither requests nor expects a state change to the target resource. GET, HEAD, OPTIONS, and TRACE are considered safe because their defined purposes are retrieval or diagnostics, not changing the target resource.

Technically, you absolutely can change a resource with a GET request. HTTP verbs don't really care what you implement in your back-end.

You could create a GET /users/{id} endpoint that wipes your entire database with a single request, instead of just retrieving the user.

But because you CAN doesn't mean you SHOULD!

The purpose of HTTP method semantics is not to physically prevent developers from doing something. It is to communicate intent and create predictable behavior across the entire ecosystem. Browsers, caches, proxies, crawlers, and other tools rely on the assumption that GET, HEAD, Trace and OPTIONS requests are safe.

So if your application has a GET endpoint whose purpose is to change business state... please stop. Seriously. You can be better than that!

OK, Now Before You Say "Actually..."

Let's address a few important exceptions.

Calling GET a safe method does not mean that absolutely nothing is allowed to change on the server.

In fact, a perfectly valid GET request may still:

  • Write access logs.
  • Update analytics or page view counters.
  • Populate or refresh server-side caches.
  • Create a user session or issue a session cookie.

However, these are all incidental side effects of serving the request. They support the operation, but they are not the purpose of it.

Check out the diagram below, which illustrates the main flow of a GET request along with some of its possible incidental side effects.

Diagram

The key idea is that the client is asking to retrieve information, not to modify the application's business state.

In other words, incidental changes to operational state can be acceptable. Making a business-state change the purpose of a safe request is not.

Idempotence

Idempotence means that making the same request multiple times has the same intended effect on the server as making it once. Methods such as GET, HEAD, PUT, DELETE, OPTIONS, and TRACE are idempotent by definition, while POST and PATCH are not guaranteed to be.

Before the "Actually" Squad Arrives Again...

Just like with safety, there are some important exceptions and nuances.

Idempotence does not mean that every repeated request produces the exact same response. The server is allowed to return different responses each time.

For example:

DELETE /users/42
Enter fullscreen mode Exit fullscreen mode

The first request might return:

204 No Content
Enter fullscreen mode Exit fullscreen mode

Because the user was successfully deleted.

The second request might return:

404 Not Found
Enter fullscreen mode Exit fullscreen mode

Because the user no longer exists. (Some might argue that the service should return 204 No Content even when the resource has already been deleted, but that discussion needs a separate article.)

The operation is still considered idempotent because the intended result is the same: after the request completes, the user is gone.

Idempotence also does not mean that the server has no side effects. A repeated PUT or DELETE request may still create logs, update metrics, or trigger internal processing. These are incidental effects, not changes to the intended resource state.

The important question is not:

"Did absolutely nothing happen twice?"

The question is:

"Did repeating the same request produce the same intended state?"

That distinction is what allows HTTP infrastructure to safely retry certain requests when needed.

Pizza Explains Everything

A good pie of pepperoni pizza is the answer to everything in this world. This topic is no exception.

Imagine ordering your favorite pizza.

A safe operation is asking the pizza place: "What pizzas do you have?" You get information, but nobody starts cooking anything.

An idempotent operation is telling them: "Replace my order with one large pepperoni pizza." If your phone accidentally sends the request five times, the result is still one large pepperoni pizza.

Pizza 1

In both scenarios, a new entry was created in the receptionist's call log. That's an incidental side effect, the business state is not affected.

A non-idempotent operation is saying:

"Add another pizza to my order."

or

"Add 10 more pepperoni slices to my pizza." Mamma mia! That's the greatest non-idempotent operation I've ever seen!

Pizza 2

...Now I want some pizza.

Safety and Idempotence Table (Before QUERY)

HTTP Method Safe Idempotent
GET Yes Yes
HEAD Yes Yes
OPTIONS Yes Yes
TRACE Yes Yes
PUT No Yes
DELETE No Yes
POST No No
PATCH No No

Enter HTTP QUERY

Why Do We Need Another HTTP Method?

Alright, enough with the prerequisites. Let's finally talk about HTTP QUERY.

As the name suggests, the QUERY method is designed for querying resources... duh.

Imagine you're building a large airport management system. It obviously has a vast number of data points and supports hundreds, if not thousands, of filtering options.

If all you need is the basic information for a specific flight, a simple GET request is exactly the right choice:

GET /flights/BA217 HTTP/1.1
Host: api.airport.example
Accept: application/json
Enter fullscreen mode Exit fullscreen mode

The server might respond with:

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=60

{
  "flightNumber": "BA217",
  "origin": "LHR",
  "destination": "IAD",
  "status": "Boarding",
  "gate": "A12"
}
Enter fullscreen mode Exit fullscreen mode

Nothing fancy here. Just a good old HTTP GET request.

Now, let's say you're not looking for one specific flight anymore. You want to build an operations dashboard that shows all flights matching certain conditions.

For example:

  • Flights departing from London Heathrow (LHR)
  • Arriving at Washington Dulles (IAD)
  • Between 6:00 PM and 10:00 PM
  • Currently delayed
  • Operated by British Airways
  • With at least 200 passengers
  • Sorted by delay duration
  • Returning only the first 50 results

A GET request might become:

GET /flights?origin=LHR&destination=IAD&departureStart=18:00&departureEnd=22:00&status=delayed&airline=BA&minPassengers=200&sort=delayDuration&page=1&limit=50 HTTP/1.1
Host: api.airport.example
Accept: application/json
Enter fullscreen mode Exit fullscreen mode

This is a very realistic example of what a request like this might look like. At first glance, everything seems perfectly fine, and theoretically, there shouldn't be any issues. And for simple cases, that's absolutely true.

However, things start to change in the real world. Once a query becomes too large or too complex, this approach begins to break down.

There are several reasons for this:

  • URI length limits are unpredictable: A request can pass through many systems: browsers, proxies, gateways, and servers, each with their own limits. The sender often cannot know the maximum supported size in advance.
  • Some data is inefficient to encode in a URI: Complex filters, nested conditions, sorting rules, and other structured data become difficult to represent and maintain once everything has to be serialized into a URL.
  • URIs are more exposed than request bodies: URLs are commonly stored in logs, browser history, analytics tools, and bookmarks, which makes putting large or sensitive query data inside them undesirable.
  • Every query representation becomes a distinct URI: Different parameter orderings or serializations can create distinct resource identifiers even when they express the same logical query. This can fragment monitoring data and make one logical operation appear as many different request targets.

I actually ran into a similar problem myself years ago while working with an API that required advanced sorting and filtering.

I tried to follow HTTP conventions and use GET, which seemed like the right approach at first. After all, I was only retrieving data, so GET was the obvious choice.

But as the number of filters, sorting options, and query parameters kept growing, the query strings started becoming massive.

Eventually, they got so large that the server basically looked at my request and said: "Nah, I ain't processing that."

And just like everyone else in this situation, I went with the most common workaround.

I am deliberately emphasizing the word workaround here because it is not the same thing as a proper solution. A solution addresses the underlying problem in the way the system was designed to work. A workaround simply gets the job done, but it may come with trade-offs and doesn't necessarily follow the original conventions.

And that workaround? HTTP POST.

Let's talk about why POST isn't the best tool for the job either.

Technically, it works. You can simply put your large, complex query inside the request body of a POST request, process it on the back end, and move on. Easy!

The request would look like the following:

POST /flights/search HTTP/1.1
Host: api.airport.example
Accept: application/json
Content-Type: application/json

{
  "origin": "LHR",
  "destination": "IAD",
  "departureStart": "18:00",
  "departureEnd": "22:00",
  "status": "delayed",
  "airline": "BA",
  "minPassengers": 200,
  "sort": "delayDuration",
  "page": 1,
  "limit": 50
}
Enter fullscreen mode Exit fullscreen mode

But here's the catch. Well, actually, there are several catches.

The first catch is caching

Unlike GET, POST responses are not reusable by caches in the usual way. HTTP does allow a POST response to be cached when it includes explicit freshness information and a Content-Location matching the POST request's target URI, but that cached response can be reused for later GET or HEAD requests, not another equivalent POST request. Most browser caches, CDNs, and proxies do not implement this pattern automatically anyway.

For complex queries, this can be a significant limitation. These requests often involve expensive filtering, sorting, database operations, or aggregations, and caching repeated results can save substantial processing time and improve performance.

Of course, you can implement custom caching on top of POST. However, now you need to solve additional problems yourself, such as creating reliable cache keys, handling invalidation, and ensuring that cached responses correctly match the original request.

Another reason why POST is not an ideal fit for these operations comes down to the semantics of the method itself

Remember what we discussed earlier about HTTP methods? POST is defined as neither safe nor idempotent. It is intended for submitting data to be processed, often resulting in some kind of state change.

A query operation is fundamentally different. When we are retrieving data, we are performing a read-only operation. We generally want it to be safe, repeatable, and compatible with HTTP's caching mechanisms.

The semantics of POST do not provide those guarantees. Along with methods like PATCH, it is simply not the natural semantic fit for this type of operation.

So why is it still commonly used for complex queries? Because POST is widely supported, familiar, and flexible. When developers hit the limitations of GET, POST becomes the obvious workaround, even if it is not the perfect tool for the job.

Think about it

If we can use POST for querying data, why can't we use PUT or DELETE instead? After all, they are idempotent, so on paper they might seem like better candidates.

But that still wouldn't make much sense.

Idempotence alone does not mean a method is suitable for reading data. PUT and DELETE are still designed for operations that modify resource state, replacing or removing resources respectively. They simply guarantee that repeating the same operation produces the same intended result.

POST became the common workaround not because it is the perfect fit, but because it is flexible, widely supported, and allows clients to send complex data in the request body. Over time, using POST for complex queries became such a common pattern that it started feeling natural, even though it doesn't perfectly match the semantics of a read-only operation.

GET with a request body?

Some of you might ask the following question: instead of using POST, why don't we just use GET? After all, the HTTP message format can technically carry content with a GET request, just like it can with POST, right?

Well, yes, at the message-format level, that is possible. But HTTP does not define generally applicable semantics for content in a GET request. In other words, you can send it, but there is no universal agreement about what anyone should do with it.

But as we've already seen several times, theory and real-world behavior are not always the same thing. Reality hits us with a left hook. Many existing systems, including some servers, proxies, and CDNs, do not reliably handle GET request bodies. Some may ignore them, reject them, or just not forward them as expected.

Meme

HTTP QUERY for the Win

HTTP QUERY combines the best parts of both worlds. Like GET, it is designed for safe, read-only operations. Like POST, it allows clients to send structured data in the request body. Its responses are cacheable, and the method can be automatically retried after something like a connection failure because repeating it is not supposed to cause business-state changes.

Here's the same example request as we had previously, but now using HTTP QUERY:

QUERY /flights HTTP/1.1
Host: api.airport.example
Accept: application/json
Content-Type: application/json

{
  "origin": "LHR",
  "destination": "IAD",
  "departureStart": "18:00",
  "departureEnd": "22:00",
  "status": "delayed",
  "airline": "BA",
  "minPassengers": 200,
  "sort": "delayDuration",
  "page": 1,
  "limit": 50
}
Enter fullscreen mode Exit fullscreen mode

One important detail here is that QUERY does not invent a universal query language. The body could contain JSON, SQL, JSONPath, form-encoded data, or another format understood by the resource. The request's Content-Type tells the server how to interpret it, and RFC 10008 requires that field to be present and consistent with the content.

Whatever format is used, processing the request must remain safe. Putting a state-changing instruction inside a QUERY body does not magically make that operation safe, it would simply violate the method's semantics.

That also gives servers a clear way to reject invalid requests:

  • A 4xx response such as 400 Bad Request when the media type is missing.
  • 400 Bad Request when the declared media type is inconsistent with the content.
  • 415 Unsupported Media Type when the resource does not support that query format.
  • 422 Unprocessable Content when the format is valid but the query itself cannot be processed.
  • 406 Not Acceptable when the server cannot produce a response in one of the formats requested through Accept.

Based on these semantics, QUERY is both safe and idempotent, making it a much better fit for complex read operations.

Now, some might argue: instead of introducing an entirely new HTTP method, why not simply update GET and officially allow request bodies?

There are two major reasons why that approach is problematic.

First, GET already has a well-established meaning: retrieving a representation of the resource identified by the request target. A complex query is not always a simple representation of a single resource. It can represent a set of filters, conditions, and operations that produce a result. Expanding GET to cover this use case would make its semantics much less clear.

Second, the existing HTTP ecosystem has been built around the assumption that GET bodies are not meaningful. Many intermediaries, caches, and CDNs were designed with this behavior in mind. Changing that assumption would require significant updates and could introduce unpredictable compatibility issues across the web.

How Do Clients Know a Server Supports QUERY?

You could simply send a QUERY request and see what happens, but HTTP provides a cleaner discovery mechanism.

A client can send an OPTIONS request:

OPTIONS /flights HTTP/1.1
Host: api.airport.example
Enter fullscreen mode Exit fullscreen mode

And the server can respond with:

HTTP/1.1 200 OK
Allow: GET, HEAD, QUERY, OPTIONS
Accept-Query: application/json
Enter fullscreen mode Exit fullscreen mode

Allow tells the client that the resource supports the QUERY method. The new Accept-Query response header goes one step further and advertises the query formats accepted by that resource.

For example, a different resource might support several formats:

Accept-Query: application/jsonpath, application/sql
Enter fullscreen mode Exit fullscreen mode

So clients don't have to blindly guess whether a server expects JSON, SQL, or interpretive dance. The resource can tell them.

Giving a Query a URI

Putting the query in the request body solves the URI-length problem, but it creates another interesting question:

"What if I want to refer to this query or its result later?"

RFC 10008 introduces the concept of an equivalent resource: a resource that represents the combination of the QUERY target, its content, and relevant metadata.

After processing our flight query, the server could respond like this:

HTTP/1.1 200 OK
Content-Type: application/json
Content-Location: /flights/results/17
Location: /flights/queries/42
ETag: "flights-42-1"

{
  "items": [
    {
      "flightNumber": "BA217",
      "origin": "LHR",
      "destination": "IAD",
      "status": "Delayed"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

These two location headers look similar, but they describe different things:

  • Content-Location identifies a resource containing the result that was just returned.
  • Location identifies the equivalent query resource, allowing the client to repeat the same operation later with a normal GET request instead of resending the query body.

The server is not required to create either resource, and any URI it provides may be temporary. But when it does, clients gain a GET-accessible link that can be cached, shared, or revisited.

A server can also skip returning the result directly and send the client to that GET-accessible resource:

HTTP/1.1 303 See Other
Location: /flights/queries/42
Enter fullscreen mode Exit fullscreen mode

The 303 See Other is important here because it explicitly tells the client to follow the redirect using GET. Other standard redirects, including 301, 302, 307, and 308, preserve the QUERY method for this operation.

A Few Things to Watch Out For

As we discussed earlier, using POST for querying data has become such a common workaround that, at this point, it almost feels natural. Many systems have been built around this pattern for years, and switching from existing POST-based query endpoints to QUERY across an entire ecosystem will require significant effort.

Another thing to be careful about is caching.

With GET requests, caching is relatively straightforward because the request target (URI) already contains the information needed to identify the requested resource. With QUERY, the request body becomes part of what defines the query itself.

That means caching implementations must take the request content and relevant metadata, such as its media type, into account when generating cache keys. Otherwise, two completely different queries sent to the same endpoint could accidentally be treated as the same cached response, which is obviously not something anyone wants.

QUERY also supports conditional requests. A client can send an If-None-Match header with an ETag from an earlier response, and the server can return 304 Not Modified when the selected query result has not changed. So QUERY doesn't merely claim to be cacheable, it can participate in the same validation model we already use with GET.

However, the fact that query content lives in the request body does not make it magically private.

Request URIs are more likely to appear in browser history, access logs, analytics systems, and intermediary logs, so moving complex query content out of the URI can reduce accidental exposure. But request bodies may still be logged, inspected, cached, or stored by your own infrastructure. If a query contains secrets, the correct solution is not to assume the body is invisible.

And if the server generates a Location or Content-Location URI for a sensitive query, that URI should not contain sensitive pieces copied from the original request body. Otherwise, we would move the data out of the URL... only to put it right back there.

There is one more browser-specific detail worth knowing: QUERY is not a CORS-safelisted method. A cross-origin browser request therefore requires a preflight OPTIONS request before the actual QUERY request can be sent.

And outside the browser, API gateways, firewalls, reverse proxies, and server configurations often use explicit method allowlists. Until they are updated, some of them may look at QUERY, raise an eyebrow, and reject it before your application ever sees the request.


Personal Take

A Step in the Right Direction

I really believe this is a step in the right direction.

As someone who values logical design and following conventions, I think conventions exist for a reason: they make our lives easier by making systems more intuitive, predictable, and less confusing.

For years, whenever I needed to implement a complex query endpoint, I usually reached for POST. And while it worked, I always had that annoying voice in my head telling me something was off, because its semantics don't perfectly match the problem we are trying to solve.

A query is a read operation. Ideally, it should be safe, idempotent, and compatible with HTTP's built-in capabilities. The semantics of POST don't provide those guarantees, which is why it always felt like a workaround rather than a perfect fit.

Don't Rush

That said, I wouldn't recommend rushing to replace every existing query endpoint with HTTP QUERY.

Adoption takes time. CDNs, API gateways, proxies, frameworks, libraries, and tooling all need to understand and properly support the new method. Without that ecosystem support, you may simply introduce compatibility problems instead of solving existing ones.

And even when QUERY is fully supported, a normal GET request remains the right choice for simple queries. GET URLs are easy to bookmark, share, paste into a browser, and inspect. If a few query parameters express the request clearly and remain within practical URI limits, there is no problem that needs fixing.

If you're maintaining legacy systems or working with older infrastructure, sticking with the traditional GET and POST approach is probably the practical choice for now. But if you're building something new and your entire stack supports HTTP QUERY, it is absolutely worth considering.

Just like with any new technology, the key is not blindly adopting it, it's understanding the problem it solves, knowing when it fits, and making sure the rest of your ecosystem is ready.

Because, as we've learned throughout this article, these kinds of problems always show up eventually.

GraphQL

One interesting area where HTTP QUERY could potentially make a lot of sense is GraphQL.

I don't know, I'm just thinking out loud.

Today, GraphQL APIs commonly send both query and mutation operations through HTTP POST. And while this works perfectly fine from a practical perspective, using POST for the read-only side has always felt a little strange from a pure HTTP semantics point of view.

After all, a GraphQL query operation is fundamentally a read operation. You are asking the server to retrieve and compose data, not modify anything. Yet we often represent that operation using a method that is not safe, not idempotent, and not naturally cache-friendly.

The reason POST became the standard approach is understandable. GraphQL queries can be large, highly structured documents with variables and nested selections, which makes a request body a natural fit.

However, this is exactly the kind of problem HTTP QUERY was designed to address: sending complex query instructions while preserving the semantics of a safe, idempotent read operation.

To be clear, this would apply only to GraphQL query operations. GraphQL mutations intentionally change state, so representing them with a safe method like QUERY would violate the method's semantics.

Whether GraphQL ecosystems will eventually adopt QUERY remains to be seen, but conceptually, the two technologies seem like a very natural match.

Conclusion

After more than three decades, HTTP is still evolving.

And that's one of the most fascinating things about it. It would be easy to assume that a protocol as fundamental as HTTP is already complete, but real-world usage always finds new challenges.

GET was never designed for massive, complex queries. POST became a common workaround, but it was never a perfect semantic fit either.

HTTP QUERY doesn't replace the methods we already have. It simply fills a gap that has existed for years: a way to send complex queries with a request body while keeping the semantics of a safe and idempotent operation.

It's not just another HTTP method. It's a reminder that even the foundations of the web can continue improving when we find a better way to build.

What are your thoughts about all this?


Enjoyed this deep dive? Let's stay connected!

I share more software engineering insights, projects, and experiments across these platforms:

Top comments (0)