Die HTTP-Methode QUERY, und wer sie wirklich unterstützt
Im Juni hat die IETF RFC 10008 veröffentlicht, die eine neue HTTP-Methode namens QUERY definiert. Die letzte standardisierte Methode war PATCH, 2010. QUERY funktioniert wie ein GET mit Body: sicher, idempotent und cachebar, und die Abfrage steht im Request-Body statt in der URL.
Das klingt nach etwas, das längst alle unterstützen sollten, also haben wir es getestet. Wir haben einen QUERY-Request an fünf öffentliche Test-APIs geschickt, unsere eigene eingeschlossen, und fünf verschiedene Antworten bekommen. Nur eine hat die Abfrage ausgeführt. Wir haben auch Node, ASP.NET Core und Python ausprobiert, und alle drei kamen damit problemlos zurecht. Im Anwendungscode ist die Methode bereit. Proxys und Server auf dem Weg sind eine andere Sache.
Das Problem, das QUERY löst
Nimm eine Produktsuche mit einem Dutzend Filtern, einer verschachtelten Preisspanne und einer Sortierung. Heute hast du zwei Möglichkeiten, und beide haben Nachteile.
- GET mit allem im Query-String. Du musst die verschachtelten Filter irgendwie in die URL kodieren. Lange URLs stoßen in Servern und Proxys an Längengrenzen, und die ganze Abfrage landet in Access-Logs, Browserverlauf und
Referer-Headern. Einen Body an ein GET zu hängen hilft nicht: RFC 9110 sagt, dass Inhalt in einem GET-Request keine festgelegte Bedeutung hat, und viele Proxys und Clients verwerfen ihn. - POST /search. Darauf läuft es bei den meisten APIs hinaus. Es funktioniert, aber POST sagt jedem Client und Proxy, dass der Request etwas ändern könnte. Also wiederholt ihn nichts automatisch nach einer abgebrochenen Verbindung, und kein Cache speichert das Ergebnis.
QUERY gibt dir einen Request-Body, den Clients, Proxys und Caches trotzdem wie ein GET behandeln können:
| GET | POST | QUERY | |
|---|---|---|---|
| Request-Body hat eine festgelegte Bedeutung | Nein | Ja | Ja |
| Sicher (ändert nichts) | Ja | Nein | Ja |
| Idempotent (darf wiederholt werden) | Ja | Nein | Ja |
| Antworten cachebar | Ja | Selten | Ja, mit dem Body im Schlüssel |
| Abfrage landet in URLs und Logs | Ja | Nein | Nein |
So sieht ein QUERY-Request aus
Er sieht aus wie ein POST, nur sagt der Methodenname allen auf dem Weg, dass er nur liest:
QUERY /products/search HTTP/1.1
Host: shop.example
Content-Type: application/json
Accept: application/json
{"category":"electronics","maxPrice":500,"inStock":true,"sort":"price"}HTTP/1.1 200 OK
Content-Type: application/json
Location: /products/search/q/7f3a9c
Cache-Control: max-age=60
[{"id":4,"name":"USB-C hub","price":39},{"id":11,"name":"Mechanical keyboard","price":89}]Der Header Location ist optional und einer der nützlicheren Teile der RFC. Er nennt eine URL, die dieselbe Abfrage mit einem einfachen GET ausführt, ein Client kann den Body also einmal senden und danach einen gewöhnlichen Link nutzen. Content-Location bedeutet etwas leicht anderes: eine URL, die genau dieses Ergebnis liefert. Ein Server kann auch ganz ohne Ergebnisse mit 303 See Other antworten, was den Client schickt, sie per GET von der URL in Location zu holen.
Was die RFC von Servern verlangt
Vor allem Strenge beim Body, denn der Body ist die Abfrage:
Content-Typeist Pflicht. Fehlt er, lehnt der Server den Request mit einem 4xx ab, meist 400. Der Server darf den Typ nicht aus dem Body erraten.- Ein Format, das die Ressource für Abfragen nicht annimmt, bekommt
415. Ein Body, der nicht zu seinem angegebenen Typ passt, bekommt400, und eine wohlgeformte Abfrage, die der Server nicht ausführen kann, bekommt422. - Ein neuer Antwort-Header
Accept-Querylistet die Abfrageformate, die eine Ressource annimmt. Er sagt Clients außerdem, dass QUERY überhaupt unterstützt wird:
HTTP/1.1 415 Unsupported Media Type
Accept-Query: application/json, "application/jsonpath"- Caches dürfen QUERY-Antworten speichern, aber der Cache-Schlüssel muss den Request-Body enthalten. Zwei Abfragen an dieselbe URL mit unterschiedlichen Filtern sind zwei verschiedene Cache-Einträge.
- Nach einer Weiterleitung mit 301 oder 302 sendet der Client das QUERY erneut an die neue URL. Clients machen nach solchen Weiterleitungen oft aus einem POST ein GET, und die RFC schließt das für QUERY aus.
Wir haben QUERY an fünf öffentliche Test-APIs geschickt
Test-APIs sind dafür gebaut, zurückzugeben, was man ihnen schickt, man würde also erwarten, dass sie die nachsichtigsten Server überhaupt sind. Hier das Skript:
// Send one QUERY request to each URL and report what came back.
const targets = [
"https://httpbin.org/anything",
"https://httpbingo.org/anything",
"https://postman-echo.com/query",
"https://jsonplaceholder.typicode.com/posts",
"https://api.testingapis.com/api/methods/any",
];
for (const url of targets) {
const res = await fetch(url, {
method: "QUERY", // capitals: fetch only uppercases the six standard methods
headers: { "content-type": "application/json" },
body: JSON.stringify({ category: "electronics", maxPrice: 500 }),
});
const type = (res.headers.get("content-type") ?? "-").split(";")[0];
const allow = res.headers.get("allow") ?? "-";
console.log(new URL(url).host.padEnd(29), res.status, type.padEnd(17), "allow:", allow);
}httpbin.org 405 text/html allow: HEAD, POST, TRACE, PUT, GET, OPTIONS, DELETE, PATCH
httpbingo.org 200 application/json allow: -
postman-echo.com 400 text/plain allow: -
jsonplaceholder.typicode.com 404 application/json allow: -
api.testingapis.com 400 text/plain allow: -Das sind fünf Server und fünf verschiedene Antworten:
| API | Antwort | Was sie einem Client sagt |
|---|---|---|
| httpbingo.org | 200 mit zurückgegebener Methode und Body | Es funktioniert. httpbingo ist eine Go-Neufassung von httpbin, und net/http von Go lässt jede Methode durch. |
| httpbin.org | 405 mit einem Allow-Header | Die richtige Art, Nein zu sagen: Die Methode wird nicht unterstützt, und hier ist die Liste der unterstützten. |
| jsonplaceholder.typicode.com | 404 | Irreführend. Die URL existiert, aber der Router fand keine Route, ein Client würde also glauben, er habe den falschen Pfad. |
| postman-echo.com | 400, 11 Bytes text/plain | Sagt, der Request sei fehlerhaft, was er nicht war. |
| api.testingapis.com | 400, 11 Bytes text/plain | Dasselbe, und das ist unsere. |
So scheitert unsere eigene API:
$ curl -i -X QUERY https://api.testingapis.com/api/methods/any \
-H "content-type: application/json" -d '{"q":"x"}'
HTTP/1.1 400 Bad Request
content-length: 11
content-type: text/plain
connection: close
Bad RequestDiese Antwort hat unsere Anwendung nie erreicht. Jede Antwort unserer ASP.NET-Core-App trägt die Header server: Kestrel und x-powered-by: TestingAPIs, und diese hat keinen davon. Die API läuft auf Azure Container Apps, und der Ingress-Proxy davor hat die Methode abgelehnt, bevor er sie weiterreichte. Postman Echo schickt denselben 11-Byte-Body und einen Header x-envoy-upstream-service-time zurück, dort passiert also offenbar dasselbe, hinter Envoy. Für eine ausgedachte Methode wie FOO bekommen wir dasselbe 400, der Proxy behandelt QUERY also als unbekannte Methode, nicht als fehlerhaften Request.
Die Lehre betrifft den Ort des Problems. Dein Framework kommt vielleicht gut mit QUERY zurecht, während ein Load Balancer, Ingress oder eine WAF davor den Request abweist, und der Fehler, den du zurückbekommst, verrät nichts davon. Teste QUERY also über jeden Hop, den ein echter Client durchläuft, nicht nur gegen localhost.
Die Frameworks unterstützen es schon
Beim Anwendungscode sieht es viel besser aus. Wir haben drei Server auf dem Rechner getestet, auf dem wir das hier schreiben, und jeder kam ohne zusätzliche Einrichtung mit QUERY zurecht.
Node.js 24. Der eingebaute HTTP-Parser kennt die Methode, und fetch sendet sie:
import http from "node:http";
console.log(http.METHODS.includes("QUERY")); // true on Node 24
http
.createServer((req, res) => {
if (req.method !== "QUERY") {
res.writeHead(405, { allow: "QUERY" }).end();
return;
}
let body = "";
req.on("data", (chunk) => (body += chunk));
req.on("end", () => {
const filters = JSON.parse(body);
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ method: req.method, filters }));
});
})
.listen(3000);ASP.NET Core 10. .NET 10 bringt HttpMethods.Query auf der Serverseite und HttpMethod.Query für HttpClient mit:
using System.Net.Http.Json;
var app = WebApplication.Create(args);
app.MapMethods("/products/search", [HttpMethods.Query], async (HttpRequest req) =>
{
var filters = await req.ReadFromJsonAsync<ProductFilter>();
return Results.Ok(new { method = req.Method, filters });
});
app.Urls.Add("http://localhost:5000");
await app.StartAsync();
// Call it with HttpClient, then try a GET on the same route.
using var http = new HttpClient();
var msg = new HttpRequestMessage(HttpMethod.Query, "http://localhost:5000/products/search")
{
Content = JsonContent.Create(new { category = "electronics", maxPrice = 500 })
};
var res = await http.SendAsync(msg);
Console.WriteLine($"QUERY: {(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}");
var get = await http.GetAsync("http://localhost:5000/products/search");
Console.WriteLine($"GET: {(int)get.StatusCode} Allow: {string.Join(", ", get.Content.Headers.Allow)}");
await app.StopAsync();
record ProductFilter(string Category, decimal MaxPrice);QUERY: 200 {"method":"QUERY","filters":{"category":"electronics","maxPrice":500}}
GET: 405 Allow: QUERYAchte auf die zweite Zeile. Ein GET an eine Route, die nur QUERY behandelt, bekommt ein 405 mit Allow: QUERY, ohne dass du dafür etwas schreibst. Das ist dieselbe höfliche Absage, die uns httpbin.org gab.
Python. http.server reicht jeden Request an einen Handler do_<METHOD>, QUERY hinzuzufügen kostet also eine Methode. Auf der Client-Seite nimmt urllib.request.Request method="QUERY" an.
from http.server import BaseHTTPRequestHandler, HTTPServer
import json
class Handler(BaseHTTPRequestHandler):
def do_QUERY(self):
length = int(self.headers.get("content-length", 0))
filters = json.loads(self.rfile.read(length))
body = json.dumps({"method": self.command, "filters": filters}).encode()
self.send_response(200)
self.send_header("content-type", "application/json")
self.end_headers()
self.wfile.write(body)
HTTPServer(("127.0.0.1", 8000), Handler).serve_forever()Schreib es groß
HTTP-Methodennamen unterscheiden Groß- und Kleinschreibung. Der fetch-Standard schreibt sechs davon für dich groß: DELETE, GET, HEAD, OPTIONS, POST und PUT. Alles andere geht genau so raus, wie du es getippt hast. Das gab ein Node-Server für drei Schreibweisen zurück:
"query" -> 400
"QUERY" -> 200 QUERY
"post" -> 200 POSTmethod: "query" geht als kleingeschriebenes query raus, und der Server antwortet mit 400. PATCH hat dieselbe Falle seit Jahren, und Node gibt eine Warnung aus, wenn du "patch" schreibst. Bei "query" warnt es nicht. Wenn ein QUERY-Request scheitert und du nicht siehst, warum, prüf die Großschreibung.
Browser schicken zuerst einen Preflight
QUERY steht nicht auf der kurzen Liste von Methoden, die Browser ohne vorherige Nachfrage cross-origin senden. Vor einem Cross-Origin-QUERY schickt der Browser also einen OPTIONS-Preflight, und der Server muss QUERY in Access-Control-Allow-Methods nennen. httpbingo war die einzige API, die unsere Abfrage ausgeführt hat, und das sagt sie einem Browser:
$ curl -i -X OPTIONS https://httpbingo.org/anything \
-H "Origin: https://example.com" \
-H "Access-Control-Request-Method: QUERY" \
-H "Access-Control-Request-Headers: content-type"
HTTP/1.1 200 OK
access-control-allow-origin: https://example.com
access-control-allow-methods: GET, POST, HEAD, PUT, DELETE, PATCH, OPTIONS
access-control-allow-headers: content-typeQUERY steht nicht in dieser Liste, derselbe Request, der aus Node funktioniert hat, würde im Browser also mit einem CORS-Fehler scheitern. Wenn deine CORS-Einstellungen die erlaubten Methoden namentlich aufführen, ergänze dort auch QUERY.
OpenAPI 3.2 kann es beschreiben
OpenAPI 3.2, erschienen im September 2025, hat Path Items eine Operation query hinzugefügt, neben get und post:
openapi: 3.2.0
info:
title: Shop
version: 1.0.0
paths:
/products/search:
query:
summary: Search products with filters in the body
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ProductFilter"
responses:
"200":
description: Matching productsOb dein Doku-Generator, Client-Generator oder API-Gateway dieses Feld schon liest, ist eine andere Frage. Prüf es, bevor du dich darauf verlässt, genau wie beim Netzwerk.
Solltest du QUERY schon nutzen?
Für Verkehr zwischen deinen eigenen Diensten, wo du jeden Hop kontrollierst, ja. Teste den ganzen Weg einmal, und du bekommst Retries und Caching, die POST /search nicht bieten kann.
Bei einer öffentlichen API würden wir es ergänzen, ohne etwas wegzunehmen:
- Lass
POST /searchweiter funktionieren und ergänze QUERY auf derselben Ressource. Clients und Netze, die kein QUERY senden können, haben so weiterhin einen Weg hinein. - Sende
Accept-Query, damit Clients erkennen, dass QUERY unterstützt wird, bevor sie es versuchen. - Gib einen
Location-Header zurück. Hat ein Client eine Abfrage einmal ausgeführt, kann er sie mit einem GET wiederholen, das durch jeden Proxy der Welt geht. - Antworte auf Routen ohne QUERY-Unterstützung mit
405und einemAllow-Header. Antworte nicht mit 400 oder 404, die ein Problem im Request des Clients nahelegen.
Wenn du prüfst, wie dein Client mit einem Server umgeht, der eine Methode ablehnt, behandelt unsere Lektion zu HTTP-Methoden 405 und Allow, und der Leitfaden zu Fehlerantworten zeigt, wie du von unserer API auf Wunsch 4xx- und 5xx-Antworten bekommst.
PATCH hat Jahre gebraucht, um zuverlässig über Clients, Proxys und Frameworks hinweg zu funktionieren, und QUERY wird wohl auch eine Weile brauchen. Der Unterschied diesmal: Der Anwendungscode ist vom ersten Tag an bereit. Gescheitert wird noch an den Proxys.