Seit 1997 kennt HTTP im Kern zwei Wege, mit einem Server zu interagieren: GET und POST. Und seitdem behelfen sich Entwickler mit Workarounds, wenn keine der beiden Methoden so richtig passt. Kennst du das – ein Suchendpunkt mit zehn Filterparametern, und du überlegst, ob du das als GET mit einer URL zusammenschraubst, die kein Mensch mehr lesen will, oder als POST, weil der Body einfach praktischer ist?
Das Problem dabei: POST ist die generische HTTP-Methode für “mach etwas Ressourcen-Spezifisches mit diesem Content” – sie sagt weder “ich bin sicher” noch “ich bin wiederholbar”. Ein Cache oder Proxy kann also nicht erkennen, ob dein POST nur Daten liest oder eine Bestellung auslöst, und behandelt ihn sicherheitshalber wie Letzteres: kein Caching, keine automatischen Retries.
Genau diese Lücke schließt jetzt eine neue HTTP-Methode. Im Juni 2026 hat die IETF mit RFC 10008 die Methode QUERY standardisiert – die erste komplett neue HTTP-Methode seit PATCH im Jahr 2010. Sie kombiniert das, was GET und POST bisher nie zusammen konnten: einen Request-Body wie bei POST, aber mit der expliziten Zusage von GET, dass dabei nichts verändert wird und Wiederholungen unbedenklich sind.
In diesem Artikel schauen wir uns an, welches Problem QUERY konkret löst, wie die Methode spezifiziert ist und wie du sie schon heute in einer Spring Boot Anwendung nutzen kannst – auch wenn die Frameworks noch nicht vollständig nachgezogen haben. Das ist der Auftakt zu einer kleinen Serie über HTTP- und API-Design, in der wir uns Stück für Stück Konzepte anschauen, die im API-Alltag oft unterschätzt werden.
Das Problem: GET, POST und die Lücke dazwischen
Stell dir vor, du baust einen Such-Endpunkt für einen Onlineshop. Der Nutzer kann nach Kategorie, Preisspanne, Verfügbarkeit, Bewertung, Versandart und zehn weiteren Kriterien filtern – plus Sortierung und Pagination. Mit GET sieht das schnell so aus:
GET /products?category=electronics&minPrice=50&maxPrice=500&inStock=true&rating=4&shipping=express&brand=sony&brand=bose&color=black&sort=-rating&page=2&size=20&tags=wireless&tags=bluetooth HTTP/1.1
Host: shop.example.org
Funktioniert – bis es nicht mehr funktioniert. Sobald Filter dynamisch aus einer Multi-Select-UI kommen oder du strukturierte Kriterien brauchst (verschachtelte Bedingungen, Freitextsuche mit Sonderzeichen), stößt du an handfeste Grenzen:
- Längenlimits: Viele Proxies, Load Balancer und ältere Server kappen URLs bei 2048 bis 8192 Zeichen. Deine Query fliegt dir dann mit einem 414 (URI Too Long) um die Ohren.
- Encoding-Hölle: Verschachtelte Strukturen oder Sonderzeichen in Query-Parametern zu kodieren ist fehleranfällig und schlecht lesbar.
- Logging: Alles, was in der URL steht, landet zuverlässig in Access-Logs, Browser-Historys und Proxy-Logs – auch wenn es eigentlich niemanden etwas angeht.
Der naheliegende Ausweg ist POST:
POST /products/search HTTP/1.1
Host: shop.example.org
Content-Type: application/json
{
"category": "electronics",
"priceRange": { "min": 50, "max": 500 },
"filters": { "inStock": true, "rating": { "gte": 4 } },
"sort": "-rating",
"page": { "number": 2, "size": 20 }
}
Der Body löst das Struktur- und Längenproblem elegant. Aber jetzt hast du ein anderes: Für den restlichen HTTP-Stack ist dieser Request unsichtbar als das, was er eigentlich ist – eine reine Leseoperation. Ein Cache darf die Antwort nicht cachen, weil POST-Responses per Spezifikation nicht standardmäßig cachebar sind. Ein Client darf den Request nicht automatisch wiederholen, wenn die Verbindung abbricht, weil POST als “potenziell nicht sicher” gilt – bei einer echten Bestellung willst du ja auch nicht, dass ein Retry sie versehentlich doppelt auslöst. Der Browser selbst warnt beim Neuladen: “Soll das Formular erneut gesendet werden?”
Das Kernproblem: GET und POST sind an ihre jeweilige Semantik gekoppelt, aber diese Semantik hängt an der falschen Eigenschaft. GET bringt “sicher und wiederholbar” mit, aber keinen Body. POST bringt einen Body mit, aber keine Sicherheitsgarantie. Für eine reine Abfrage mit komplexem Input brauchst du eigentlich beides gleichzeitig – und genau dafür gab es bisher keine Methode.
Die Lösung: QUERY
QUERY beantwortet genau dieses Problem, indem sie sich das jeweils Beste von GET und POST leiht. Das gleiche Beispiel von eben sieht mit QUERY so aus:
QUERY /products HTTP/1.1
Host: shop.example.org
Content-Type: application/json
Accept: application/json
{
"category": "electronics",
"priceRange": { "min": 50, "max": 500 },
"filters": { "inStock": true, "rating": { "gte": 4 } },
"sort": "-rating",
"page": { "number": 2, "size": 20 }
}
Syntaktisch fast identisch zum POST-Beispiel – der Unterschied steckt komplett im Methodennamen. Und der macht den entscheidenden Unterschied: QUERY ist per Definition safe (der Client verlangt und erwartet keine Zustandsänderung) und idempotent (der Request kann gefahrlos wiederholt werden, etwa nach einem Verbindungsabbruch). Damit signalisiert QUERY dem gesamten HTTP-Stack – Caches, Proxies, Clients – explizit das, was bei POST reine Vermutung wäre.
Wichtig: RFC 10008 verlangt einen Content-Type-Header. Fehlt er oder passt er nicht zum tatsächlichen Body, muss der Server den Request ablehnen – Content-Sniffing ist ausdrücklich nicht erlaubt. Das ist kein Zufall: Der Content-Type ist bei QUERY das, was bei GET die Query-Parameter sind – er definiert zusammen mit dem Body, was überhaupt abgefragt wird.
So ordnet sich QUERY zwischen GET und POST ein:
| GET | QUERY | POST | |
|---|---|---|---|
| Safe | ja | ja | potenziell nein |
| Idempotent | ja | ja | potenziell nein |
| Body | keine definierte Semantik | erwartet (Semantik abhängig von der Zielressource) | erwartet (Semantik abhängig von der Zielressource) |
| Cachebar | ja | ja | nur eingeschränkt, für spätere GET/HEAD |
Der Server kann sagen, was er kann
Damit ein Client nicht raten muss, welches Query-Format ein Endpunkt versteht, definiert RFC 10008 den neuen Response-Header Accept-Query. Ein Server kann so schon auf OPTIONS- oder GET-Anfragen antworten:
Accept-Query: "application/jsonpath", application/sql;charset="UTF-8"
Das sagt dem Client: Dieser Endpunkt akzeptiert QUERY-Bodies in JSONPath oder SQL, sonst nichts. Schickst du ein nicht unterstütztes Format, antwortet der Server mit 415 (Unsupported Media Type) – ist das Format zwar bekannt, aber der Inhalt syntaktisch oder inhaltlich falsch, sind je nach Fall 400 (Bad Request) oder 422 (Unprocessable Content) korrekt.
Caching ist möglich, aber nicht umsonst
Der Haken an der Sache: Bei GET reicht die URL als Cache-Key. Bei QUERY muss ein Cache den kompletten Request-Body einbeziehen, um zu wissen, ob zwei Anfragen wirklich identisch sind – das ist aufwendiger, aber machbar, und RFC 10008 beschreibt es explizit. Praktischer wird es, wenn der Server in der Antwort einen Content-Location- oder Location-Header mitschickt, der auf eine feste URI für das Ergebnis zeigt. Ab dann kann der Client für Folgeanfragen einfach GET auf diese URI nutzen – mit ganz normalem, günstigem URL-basiertem Caching.
QUERY in der Praxis: Spring Boot
Der ernüchternde Fakt zuerst: Spring Framework kennt bisher kein RequestMethod.QUERY. Die RequestMethod-Enum, auf der @RequestMapping(method = ...) und die Shortcuts wie @GetMapping oder @PostMapping basieren, deckt nur GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS und TRACE ab. Das Spring-Team verfolgt die native Unterstützung bereits – nachzulesen in Issue #36988 im spring-framework Repository – aber solange das nicht gemerged ist, kannst du QUERY nicht deklarativ mappen wie eine der Standardmethoden.
Das heißt aber nicht, dass du QUERY heute nicht nutzen kannst. Du musst nur einen Schritt manuell gehen, den Spring dir für die Standardmethoden abnimmt: die Methode selbst prüfen.
@RestController
@RequestMapping("/products")
public class ProductQueryController {
private final ProductSearchService productSearchService;
public ProductQueryController(ProductSearchService productSearchService) {
this.productSearchService = productSearchService;
}
// Kein RequestMethod.QUERY verfügbar -> Mapping ohne method-Filter,
// dafür wird die Methode im Handler selbst geprüft.
@RequestMapping(
path = "",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<ProductSearchResult> query(
HttpServletRequest request,
@RequestBody ProductQuery query) {
if (!"QUERY".equalsIgnoreCase(request.getMethod())) {
return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED)
.header("Allow", "QUERY")
.build();
}
ProductSearchResult result = productSearchService.search(query);
return ResponseEntity.ok(result);
}
}
public record ProductQuery(
String category,
PriceRange priceRange,
Filters filters,
String sort,
Page page
) {}
public record PriceRange(int min, int max) {}
public record Filters(boolean inStock, RatingFilter rating) {}
public record RatingFilter(int gte) {}
public record Page(int number, int size) {}
Wichtig dabei: Der eingebettete Servlet-Container (Tomcat, Jetty) muss den QUERY-Verb überhaupt durchlassen. Das ist unkritisch, da HTTP-Methoden als generische Tokens behandelt werden – Tomcat validiert nur die Syntax, nicht gegen eine feste Liste bekannter Methoden. @RequestBody funktioniert unabhängig von der HTTP-Methode, weil Spring dafür nur Content-Type und den Stream auswertet, nicht die Methode selbst.
Pro-Tipp: Setze den Accept-Query-Header selbst, solange Spring das nicht automatisch tut:
@RequestMapping(path = "", method = RequestMethod.OPTIONS)
public ResponseEntity<Void> options() {
return ResponseEntity.ok()
.header("Accept-Query", "\"application/json\"")
.build();
}
Sobald Spring Framework 7.1 natives RequestMethod.QUERY mitbringt, wird aus dem Workaround vermutlich wieder ein einfaches @RequestMapping(method = RequestMethod.QUERY) – die manuelle Prüfung im Handler kannst du dann ersatzlos streichen.
Pro-Tipps / Warnungen
Warnung: QUERY zählt nicht zu den CORS-safelisted methods. Rufst du einen QUERY-Endpunkt von einer anderen Origin aus dem Browser auf, schickt der Browser vorher automatisch einen Preflight-Request (OPTIONS). Vergisst du,
QUERYinAccess-Control-Allow-Methodsfreizugeben, scheitert der eigentliche Request mit einem CORS-Fehler, ohne dass dein Server-Log dazu viel hergibt.
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedMethods(List.of("GET", "POST", "QUERY"));
config.setAllowedOrigins(List.of("https://app.example.org"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/products/**", config);
return source;
}
Tipp: Unterscheide sauber zwischen den drei Fehlerfällen, die RFC 10008 für QUERY vorsieht. Das ist kein Nice-to-have, sondern hilft Clients, automatisch richtig zu reagieren:
- 415 (Unsupported Media Type): Das Format an sich wird nicht unterstützt (z. B. XML statt JSON).
- 400 (Bad Request): Format ist korrekt deklariert, aber Content-Type und Body passen nicht zusammen, oder die Syntax ist fehlerhaft.
- 422 (Unprocessable Content): Syntaktisch alles sauber, aber inhaltlich unmöglich – etwa eine Filterbedingung auf ein Feld, das es nicht gibt.
Ein Client, der nur pauschal auf 4xx reagiert, verschenkt genau die Information, für die diese Statuscodes gemacht sind.
Warnung: Nur weil Daten im Body statt in der URL stecken, heißt das nicht, dass sie automatisch aus jedem Log verschwinden. Baust du – wie im Abschnitt “Die Lösung: QUERY” beschrieben – eine
Location- oderContent-Location-URI für die Query-Ergebnisse, achte darauf, dass diese URI selbst keine sensiblen Teile der ursprünglichen Anfrage enthält (z. B. eine generierte ID statt der Rohdaten). Die URI landet nämlich wieder ganz klassisch in Access-Logs, Browser-History und Proxy-Caches.
Fazit
QUERY ist kein Grundpfeiler-Umbau von HTTP, sondern das Schließen einer Lücke, die seit fast 30 Jahren offen stand: eine Methode, die so sicher und cachebar ist wie GET, aber Daten so flexibel transportiert wie POST. Für Such- und Filter-Endpunkte mit komplexem Input ist das genau das fehlende Werkzeug – kein Zusammenschrauben überlanger Query-Strings mehr, aber auch kein Body-Request mehr, der dem Rest des HTTP-Stacks vorgaukelt, er könnte irgendwas verändern.
Auf Framework-Ebene ist die Unterstützung noch jung – wie du gesehen hast, brauchst du in Spring Boot aktuell noch einen kleinen manuellen Workaround, bis RequestMethod.QUERY nativ landet. Das ist typisch für den Beginn einer neuen HTTP-Methode: Die Spezifikation steht, die Ökosysteme ziehen nach. Genau deshalb lohnt es sich, sie jetzt schon zu kennen, statt erst zu reagieren, wenn andere Teams oder APIs sie einfach voraussetzen.
Das war der Auftakt zu einer kleinen Serie über HTTP- und API-Design. Als Nächstes schauen wir uns an, wie du mit HTTP-Caching-Headern wirklich arbeitest, statt sie nur aus Tutorials zu kopieren.
