HTTPWriting
Jul 2, 2026
8 min read
Networking

RFC 10008: HTTP Finally Gets a QUERY Method

Somewhere in your codebase there is a GET endpoint with a query string that has outgrown its welcome — filters, sort keys, pagination cursors, a JSON blob someone base64-encoded and jammed into a parameter…

Somewhere in your codebase there is a GET endpoint with a query string that has outgrown its welcome — filters, sort keys, pagination cursors, a JSON blob someone base64-encoded and jammed into a parameter…

RFC 10008: HTTP Finally Gets a QUERY Method

Somewhere in your codebase there is a GET endpoint with a query string that has outgrown its welcome. Filters, sort keys, pagination cursors, a JSON blob someone base64-encoded and jammed into a parameter because the framework wouldn’t let them do anything else. Every proxy between the client and your server has an opinion about how long that URL is allowed to be, and none of those opinions agree with each other.

RFC 10008 gives that endpoint a proper home. Published by the IETF in June 2026 as a Proposed Standard, it defines a new HTTP method: QUERY. It closes a gap that has existed in HTTP since the beginning — a request that is safe and idempotent like GET, but that carries a body like POST.

Problem

HTTP has always had a mismatch between the two methods people reach for when reading data with parameters.

GET puts everything in the URI. That’s correct semantically — the resource is fully identified by the URI, so caching and retries just work — but it breaks down for four concrete reasons, all called out directly in the RFC:

  • Nobody knows the real size limit ahead of time, because a request can pass through several uncoordinated intermediaries. RFC 9110 only recommends 8000 octets as a floor.
  • Structured data (nested filters, arbitrary JSON) is expensive and awkward to encode into a URI-safe string.
  • URIs get logged, bookmarked, and passed around far more readily than request bodies, which is a problem the moment the query contains anything sensitive.
  • Every distinct combination of parameters becomes, in principle, a distinct resource, which is rarely what you actually mean.

POST solves the size and encoding problems by moving the payload into the request body. But it throws away the one property that made GET useful in the first place: a client, proxy, or cache has no way to know, just from the method name, whether a POST is safe to retry or safe to cache. Some POST endpoints mutate state. Some don’t. HTTP semantics can’t tell the difference, so intermediaries assume the worst.

QUERY is the method that was missing: request semantics of GET, request body of POST.

Deep Dive

The RFC’s own comparison table is the cleanest way to see what changed:

GETQUERYPOST
Safeyesyespotentially no
Idempotentyesyespotentially no
URI for query itselfyes, by definitionoptionalno
Cacheableyesyesonly for future GET/HEAD
Bodyno defined semanticsexpectedexpected

A few things worth understanding properly, because they’re the parts that will bite you if you skip them.

Safety and idempotency are declared, not inferred. Because QUERY is registered in the IANA HTTP Method Registry as safe and idempotent, any generic HTTP client, proxy, or cache can treat it that way without knowing anything about your specific API. That’s the entire point: it moves a guarantee from “documented in our API docs, hopefully read” to “encoded in the protocol, mechanically enforceable.”

The “equivalent resource” concept. A QUERY request doesn’t have to remain anonymous. The RFC defines the equivalent resource as a conceptual resource derived from the target resource plus the request content. A server can assign that resource a real URI and return it two different ways:

  • Content-Location: “here’s where the result of this specific query lives.” Useful for caching the answer.
  • Location: “here’s a URI you can GET to repeat this exact query without resending the body.” Useful for turning an expensive QUERY into a cheap, cacheable GET on subsequent requests.

Get these two confused and your caching layer will serve stale or wrong data. Content-Location points at a snapshot. Location points at a live, repeatable query.

Caching a body is harder than caching a URI. Section 2.7 is blunt about this: the cache key for a QUERY response must incorporate the full request content, not just the URI. That means a cache has to read and normalise the entire body before it can even check for a hit. The RFC allows caches to strip semantically insignificant differences (content encoding, JSON formatting) to improve hit rates, but that normalisation has to match how your resource actually interprets the query, or you’ll get false-positive cache hits, which the Security Considerations section flags explicitly.

Content negotiation gets a dedicated header. Accept-Query lets a resource advertise which query languages it understands — SQL, JSONPath, XSLT, whatever — using HTTP Structured Fields syntax:

Accept-Query: "application/jsonpath", application/sql;charset="UTF-8"

A client can check this via HEAD or OPTIONS before committing to a query format, rather than guessing and getting a 415.

Method name history. The registry already had three safe, idempotent methods with bodies: PROPFIND, REPORT, SEARCH, all descended from WebDAV. The working group considered reusing one of them and rejected it: those methods bind to a single generic media type (application/xml), whereas QUERY’s semantics come entirely from whatever Content-Type you send. QUERY also just reads better against a URI’s query component, which is the whole reason it exists.

Code Examples

Go doesn’t have a http.MethodQuery constant yet (the standard library method constants predate this RFC), but that’s fine — http.Request.Method is just a string, and Go 1.22’s ServeMux supports method-based routing on arbitrary method names.

Server side:

package main
 
import (
	"encoding/json"
	"io"
	"log"
	"net/http"
)
 
type contactQuery struct {
	Select []string `json:"select"`
	Limit  int      `json:"limit"`
	Match  string   `json:"match"`
}
 
func handleContactsQuery(w http.ResponseWriter, r *http.Request) {
	if r.Header.Get("Content-Type") != "application/json" {
		http.Error(w, "unsupported media type", http.StatusUnsupportedMediaType)
		return
	}
 
	body, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "bad request", http.StatusBadRequest)
		return
	}
	defer r.Body.Close()
 
	var q contactQuery
	if err := json.Unmarshal(body, &q); err != nil {
		// Content-Type matched but content is malformed: 400, not 415.
		http.Error(w, "bad request", http.StatusBadRequest)
		return
	}
 
	results, err := runContactQuery(r.Context(), q)
	if err != nil {
		// Valid JSON, valid query shape, but the query itself failed
		// (e.g. references a field that doesn't exist): 422, per RFC 10008 §2.1.
		http.Error(w, "unprocessable content", http.StatusUnprocessableEntity)
		return
	}
 
	// Point future GET requests at a repeatable, cacheable equivalent resource.
	w.Header().Set("Location", "/contacts/stored-queries/"+results.QueryID)
	w.Header().Set("Content-Type", "application/json")
	w.Header().Set("Accept-Query", `"application/json"`)
	w.WriteHeader(http.StatusOK)
	json.NewEncoder(w).Encode(results.Rows)
}
 
func main() {
	mux := http.NewServeMux()
	// Go 1.22+ pattern syntax: method-aware routing on any method string.
	mux.HandleFunc("QUERY /contacts", handleContactsQuery)
	mux.HandleFunc("GET /contacts/stored-queries/{id}", handleStoredQuery)
 
	log.Fatal(http.ListenAndServe(":8080", mux))
}

Client side, issuing the request is equally unremarkable, which is the appeal:

func fetchContacts(ctx context.Context, client *http.Client, q contactQuery) ([]Contact, error) {
	body, err := json.Marshal(q)
	if err != nil {
		return nil, err
	}
 
	req, err := http.NewRequestWithContext(ctx, "QUERY", "https://example.org/contacts", bytes.NewReader(body))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept", "application/json")
 
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
 
	// Because QUERY is safe and idempotent, retrying this exact request
	// after a transport failure is always correct, unlike POST.
	var contacts []Contact
	return contacts, json.NewDecoder(resp.Body).Decode(&contacts)
}

One thing to plan for if you’re calling this from a browser: QUERY is not on the CORS safelisted method list, so every cross-origin call triggers an OPTIONS preflight. That’s a real latency cost on hot paths, and the RFC’s Security Considerations section says as much. It’s rarely a reason not to use QUERY, but it’s a reason not to be surprised when your network waterfall grows an extra hop.

Lessons Learned

Three things stood out while working through the spec against a real service design.

The Location versus Content-Location distinction is where most implementations will get sloppy. It’s tempting to treat them as interchangeable “here’s a URL for what you just got,” but they answer different questions — repeat the query, versus fetch this exact snapshot — and conflating them produces caching bugs that only show up once the underlying data changes between requests.

Caching QUERY is not a drop-in replacement for caching GET. Anyone running a CDN or reverse proxy in front of a QUERY-enabled API needs to actually read Section 2.7. A cache that doesn’t parse and normalise the request body before keying on it will either miss constantly or, worse, collide on requests that only look the same after truncation.

Adoption is going to be gradual and worth watching rather than reaching for immediately, since browser and intermediary support for a method that was standardised in June 2026 is still catching up. QUERY is the right primitive for the GET/POST gap, but existing infrastructure — some proxies, some SDKs, some corporate firewalls with method allowlists — will need to catch up before it’s safe as the only way to reach an endpoint. A sane rollout path is to add QUERY alongside an existing POST-based search endpoint, advertise it via Accept-Query, and let clients migrate as their tooling supports it.

Takeaways

  • QUERY fills a genuine gap: safe and idempotent like GET, with a body like POST.
  • Safety and idempotency are protocol-level guarantees via IANA registration, not documentation you hope people read.
  • Location points at a repeatable query; Content-Location points at a result snapshot. They are not interchangeable.
  • Caching a QUERY response requires keying on the full, normalised request body, which is materially more work than caching a GET.
  • Accept-Query lets a resource advertise supported query languages before a client guesses wrong and eats a 415.
  • Expect a CORS preflight on every cross-origin QUERY call from a browser; it’s not in the safelisted method set.
  • Roll it out alongside existing POST search endpoints rather than as a hard cutover, given how recent the standard is.

By Ajitem Sahasrabuddhe on July 2, 2026.