12k
All articles

HTTP adopte une nouvelle méthode pour la recherche

Découvrez HTTP QUERY, méthode sûre et idempotente pour les recherches avec corps de requête. Voyez comment fonctionnent cache et reprises, et où elle est prise en charge.

OpenReplay Team
OpenReplay Team
HTTP adopte une nouvelle méthode pour la recherche

La méthode HTTP QUERY, définie dans la RFC 10008 en juin 2026, place la requête dans le corps, comme le fait POST, tout en étant sûre et idempotente comme GET. Les réponses peuvent donc être mises en cache, et une requête ayant échoué peut être relancée automatiquement.

La plupart des équipes API connaissent ce scénario : un endpoint de recherche démarre sous la forme d’un GET bien propre, puis finit en une URL où s’entassent filtres imbriqués, plages de dates et clés de tri encodés. Quelqu’un finit par le basculer en POST, et la mise en cache comme les nouvelles tentatives cessent de fonctionner.

Cet article présente le problème que résout QUERY, le déroulement d’un échange QUERY au niveau du protocole, ainsi que les composants de la stack qui l’acceptent et ceux qui le rejettent encore.

Points clés

  • QUERY est une méthode HTTP sûre et idempotente qui transporte sa requête dans le corps. Elle est standardisée par la RFC 10008 (juin 2026).
  • Un serveur doit rejeter une requête QUERY dont le Content-Type est absent ou ne correspond pas au corps. Les caches qui stockent les réponses QUERY intègrent le corps de la requête dans la clé de cache.
  • Un script peut envoyer une requête QUERY avec fetch(), mais un formulaire HTML avec method="query" se rabat sur GET et abandonne le corps.
  • Une requête QUERY cross-origin émise depuis un navigateur déclenche systématiquement une requête de prévol (preflight) CORS, car QUERY ne fait pas partie des méthodes CORS-safelisted.
  • Tout serveur, proxy ou WAF qui ne reconnaît pas la méthode peut la rejeter avant même que votre application ne reçoive la requête.

Pourquoi les endpoints de recherche se retrouvent-ils coincés entre GET et POST ?

Ni GET ni POST ne conviennent vraiment aux recherches complexes. GET place tous les filtres dans l’URL. POST les déplace dans le corps, mais signale à tous les intermédiaires que la requête est susceptible de modifier l’état du serveur.

La RFC 9110 demande à tout émetteur et destinataire de prendre en charge des URI d’au moins 8 000 octets. Il s’agit d’un minimum, et non d’un maximum : HTTP ne fixe aucune limite supérieure, si bien que n’importe quel proxy, passerelle ou serveur sur le chemin peut rejeter une URL plus longue. Les longues chaînes de requête se retrouvent en outre dans l’historique du navigateur, les journaux serveur et les favoris, exposant ainsi le contenu des filtres.

POST évite ces écueils, mais cette méthode n’est ni sûre ni idempotente. Une requête idempotente produit le même effet attendu, qu’elle soit envoyée une fois ou dix fois. La RFC 9110 demande aux clients de ne pas relancer automatiquement une requête non idempotente, sauf s’ils disposent d’un moyen de savoir qu’elle l’est en pratique. En conséquence, les caches ne réutilisent généralement pas les réponses POST, et les passerelles ne relancent pas un POST qui échoue en cours de route.

Qu’est-ce que la méthode HTTP QUERY ?

Selon la RFC 10008, une requête QUERY demande au serveur d’exécuter la requête décrite dans le corps et d’en renvoyer le résultat, sans rien modifier côté serveur. De ce fait, un client ou un proxy peut renvoyer ou relancer une requête QUERY sans craindre qu’une première tentative interrompue ait modifié l’état du serveur, ce qu’il ne peut pas présumer sans risque avec POST. Voici la requête telle qu’elle transite sur le réseau (consultez l’anatomie d’une requête HTTP pour le détail de ses composants) :

QUERY /products/search HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json

{"category": "laptops", "price": {"max": 1500}, "brands": ["acme", "globex"], "sort": "price_asc"}

La requête est définie par le corps associé à son type de média. Si l’en-tête Content-Type est absent ou ne correspond pas au contenu du corps, la RFC 10008 impose au serveur de rejeter la requête. Les recommandations de la RFC 10008 concernant les codes de statut préconisent un code 4xx, par exemple 400 lorsqu’aucun type de média n’est indiqué, et 415 lorsque la ressource ne prend pas en charge le type de média envoyé.

Une réponse pourrait ressembler à ceci :

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=60
Accept-Query: application/json
Content-Location: /products/search/results/7f3a
Location: /products/search/queries/7f3a

{"results": [ ... ]}

Chaque en-tête remplit un rôle précis :

  • Clé de cache : les réponses QUERY peuvent être mises en cache. Un cache qui les stocke intègre le corps de la requête dans la clé de cache, car deux requêtes QUERY vers la même URL avec des corps différents posent des questions différentes.
  • Accept-Query : un serveur envoie cet en-tête de réponse pour indiquer les formats de requête qu’il accepte, sous la forme d’une liste de types de média au format Structured Field. Une même valeur s’applique à toutes les URI du serveur partageant le même chemin, quelle que soit la chaîne de requête.
  • Content-Location et Location : dans une réponse QUERY, Content-Location identifie le résultat de cette requête précise. Location fournit une URI pour la requête elle-même, que le client pourra ensuite récupérer avec un simple GET. L’article sur le contenu d’une réponse HTTP décrit le comportement habituel de ces en-têtes.

Dans l’exemple, Cache-Control relève d’une configuration de cache classique. La RFC 10008 ne l’impose pas.

Où QUERY fonctionne-t-elle aujourd’hui ?

La méthode HTTP QUERY fonctionne déjà dans plusieurs runtimes serveur, frameworks et clients HTTP, à condition qu’aucun élément situé entre le client et le serveur ne la rejette.

  • Node.js accepte QUERY dans son module http. Son parseur intégré, llhttp, définit HTTP_QUERY (voir la copie de llhttp.h dans Node) et reconnaît la méthode depuis llhttp 9.2. D’après le changelog de Node.js, cette version du parseur a été livrée dans la v21.7.2, et elle est incluse dans la v22 et les suivantes ainsi que dans la v20.19.2. Sur ces versions, http.METHODS inclut 'QUERY' et un handler serveur voit req.method === 'QUERY'.
  • Le client HTTP de Go accepte n’importe quel token de méthode valide comme chaîne de méthode dans http.NewRequest ; il peut donc déjà envoyer des requêtes QUERY :
package main

import (
	"log"
	"net/http"
	"strings"
)

func main() {
	body := strings.NewReader(`{"category":"laptops","price":{"max":1500}}`)
	req, err := http.NewRequest("QUERY", "https://api.example.com/products/search", body)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
}

Dans le navigateur, fetch() peut envoyer une requête QUERY depuis un script sur la même origine. L’issue Fetch consacrée à QUERY (whatwg/fetch#1938) souligne que le Fetch Standard n’interdit pas QUERY, mais n’en normalise pas non plus la casse. Les navigateurs envoient la chaîne de méthode exactement telle que vous l’écrivez : utilisez donc toujours des majuscules.

const res = await fetch('/products/search', {
  method: 'QUERY',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ category: 'laptops', price: { max: 1500 } }),
});
const data = await res.json();

Qu’est-ce qui ne prend pas encore en charge QUERY ?

Trois obstacles freinent aujourd’hui la méthode HTTP QUERY : les formulaires HTML ne peuvent pas l’envoyer, certains serveurs et couches de sécurité la rejettent purement et simplement, et les appels cross-origin nécessitent une requête de prévol.

Les formulaires. Les scripts peuvent envoyer des requêtes QUERY avec fetch(), mais pas les formulaires. La proposition visant à prendre en charge method="query" fait l’objet de l’issue WHATWG HTML #12594. Elle est toujours ouverte et porte le label « needs implementer interest ». En attendant qu’elle aboutisse, ce balisage :

<form method="query" action="/products/search">
  <input name="category" value="laptops">
  <button>Search</button>
</form>

arrive au serveur sous la forme d’un GET, sans son corps, car les navigateurs traitent toute méthode de formulaire inconnue comme un GET. Une démonstration en ligne conçue pour l’issue illustre ce comportement.

Serveurs, passerelles et WAF. Tout intermédiaire qui ne reconnaît pas la méthode peut la rejeter. La même démonstration indique par exemple que LiteSpeed répond à une requête QUERY par un code 400 avant même l’exécution de l’application. Reverse proxies, load balancers, CDN, passerelles API et pare-feu doivent chacun être vérifiés séparément.

Requête de prévol cross-origin. QUERY ne figure pas dans la liste des méthodes CORS-safelisted du Fetch Standard. Une requête QUERY cross-origin déclenche donc systématiquement une requête de prévol, quels que soient ses en-têtes :

OPTIONS /products/search HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: QUERY
Access-Control-Request-Headers: content-type

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, QUERY
Access-Control-Allow-Headers: content-type

Si Access-Control-Allow-Methods ne mentionne pas QUERY, le navigateur bloque la requête proprement dite.

EnvironnementStatut
fetch(), même origineFonctionne
fetch(), cross-originFonctionne après une requête de prévol autorisant QUERY
Formulaires HTMLRepli sur GET, corps abandonné
Node.js httpFonctionne (v20.19.2+, v21.7.2+, v22+ ; llhttp 9.2 reconnaît QUERY)
Client HTTP GoFonctionne (chaîne de méthode personnalisée)
Laravel 13.19+Fonctionne (client, tests, Route::match)
LiteSpeedRejet avec un code 400 avant l’exécution de l’application (signalé dans la démonstration de l’issue #12594)
Proxies, CDN, passerelles, WAFDépend du produit et de sa configuration

GraphQL doit-il utiliser QUERY ?

QUERY convient aux opérations de type query de GraphQL, qui se contentent de lire des données et correspondent donc bien à une méthode sûre, idempotente et dotée d’un corps. Les mutations modifiant l’état, elles restent du ressort de POST. Si vous hésitez entre ces deux styles d’API, l’article GraphQL vs REST détaille les compromis. L’argument en faveur de QUERY est le même dans les deux cas : les lectures de type recherche retrouvent la mise en cache et les nouvelles tentatives au niveau HTTP.

Ce qui doit encore évoluer pour que QUERY fonctionne de bout en bout

Pour que QUERY fonctionne partout, chaque couche située entre l’utilisateur et votre handler doit la reconnaître :

  1. La spécification HTML doit accepter method="query", et les navigateurs doivent l’implémenter.
  2. Chaque intermédiaire (serveur d’origine, reverse proxy, load balancer, CDN, passerelle API et WAF) doit analyser et transmettre la méthode.
  3. Les frameworks doivent router les requêtes QUERY vers les handlers et en analyser le corps.
  4. Les caches doivent intégrer le corps de la requête et le Content-Type dans la clé de cache avant de stocker les réponses QUERY.

La requête de prévol CORS est voulue : la RFC 10008 la prévoit, et l’issue whatwg/fetch#1938 ne demande aucune modification de la safelist. La question encore ouverte porte sur l’opportunité, pour le cache HTTP de Fetch, fondé sur l’URL, de prendre en charge une mise en cache indexée sur le corps pour QUERY.

Conclusion

QUERY corrige une incohérence de longue date dans HTTP : les requêtes de recherche peuvent désormais transporter un corps sans renoncer à la mise en cache ni aux nouvelles tentatives sûres. Les scripts, les clients Go et les applications Laravel peuvent déjà l’utiliser. Ce n’est pas le cas des formulaires, et les infrastructures qui ne reconnaissent pas la méthode risquent encore de la rejeter. Avant de migrer un endpoint, envoyez une véritable requête QUERY à travers l’intégralité de votre chaîne de production, CDN, passerelle et WAF compris, et conservez l’endpoint POST tant que chaque intermédiaire ne la laisse pas passer.

FAQ

Pourquoi ne pas simplement envoyer un corps de requête avec GET plutôt que d'utiliser QUERY ?

Un corps dans une requête GET n'est pas fiable. La section 9.3.1 de la RFC 9110 indique qu'un corps dans un GET n'a pas de signification généralement définie et ne peut modifier ni la cible ni le sens de la requête. Certains serveurs rejettent purement et simplement ces requêtes, car un corps GET peut servir à des attaques de type request smuggling. Par ailleurs, le corps ne fait pas partie de la clé de cache standard. Avec QUERY, le corps constitue la requête elle-même et il est intégré à la clé de cache.

Quelle est la différence entre la méthode QUERY et la méthode SEARCH de WebDAV ?

SEARCH est une méthode WebDAV, définie dans la RFC 5323 en 2008, destinée à rechercher des ressources DAV à l'aide de grammaires de requête XML. QUERY est une méthode HTTP généraliste, compatible avec n'importe quel format de requête. Les deux méthodes sont sûres, mais SEARCH ne s'est jamais diffusée au-delà de WebDAV. Les premiers brouillons de la spécification QUERY utilisaient d'ailleurs le nom SEARCH. Les auteurs ont opté pour QUERY parce que SEARCH et les autres méthodes sûres existantes proviennent de WebDAV et reposent sur un type de média XML générique, et parce que le nom QUERY fait écho à la partie query d'une URI.

Puis-je documenter des endpoints QUERY dans OpenAPI ?

Oui, à partir d'OpenAPI 3.2.0, publiée en septembre 2025. Cette version prend nativement en charge la méthode query : une opération QUERY peut ainsi déclarer un schéma requestBody et des réponses, exactement comme get ou post. Les autres méthodes non standard se déclarent dans la nouvelle map additionalOperations. OpenAPI 3.0 et 3.1 ne proposent aucun champ pour QUERY, et la prise en charge de la version 3.2 varie d'un générateur à l'autre.

Quel code de statut un serveur renvoie-t-il lorsqu'il ne prend pas en charge QUERY ?

Selon la RFC 9110, un serveur qui ne reconnaît pas une méthode de requête doit répondre par un code 501 Not Implemented. Un serveur qui reconnaît QUERY mais ne l'autorise pas sur une ressource donnée renvoie un code 405 Method Not Allowed, accompagné d'un en-tête Allow listant les méthodes qu'il prend en charge. Les proxies et les WAF peuvent échouer différemment, par exemple avec un simple code 400 : toute logique de repli vers POST doit donc gérer ces trois cas.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.