Back-end

- Méthode HTTP QUERY : le guide complet 2026

Http Query Rest-Api Http-Method Rfc-10008 Get-Vs-Post Api-Design Cache-Http Idempotence Fetch-Api Cors Node-Js Backend Web-Semantique
Méthode HTTP QUERY : le guide complet 2026

Découvrez la méthode HTTP QUERY (RFC 10008) : sémantique sûre et idempotente, corps de requête, cache, avec exemples fetch, Node.js et Angular.

Pourquoi une nouvelle méthode HTTP ?

En bref : depuis toujours, les recherches complexes coincent entre GET (sûr et cacheable, mais sans corps utile) et POST (corps riche, mais sémantiquement faux pour lire). QUERY est le chaînon manquant : un corps de requête plus une sémantique de lecture sûre.

Chaque développeur back-end a un jour buté sur le même mur. Vous construisez un endpoint de recherche avancée — filtres imbriqués, tri multi-colonnes, facettes, pagination — et vous devez choisir entre deux méthodes HTTP, aucune ne convenant vraiment.

L'impasse de GET

GET est la méthode idéale pour lire : elle est sûre (elle ne modifie rien), idempotente (la rejouer ne change rien) et cacheable. Mais son corps de requête est sémantiquement indéfini : les serveurs, proxies et navigateurs l'ignorent. Résultat, tout doit passer dans l'URL.

Un filtre un peu sérieux devient vite ce genre de monstre :

# Recherche produits : la lisibilité explose, et l'URL a une limite
GET /products?category=keyboards&inStock=true&minPrice=50&maxPrice=200&sort=price:asc,rating:desc&brands=logitech,keychron,corsair&tags=mechanical,wireless&page=2&size=20 HTTP/1.1
Host: example.com

Les problèmes s'accumulent :

  • Limite de longueur : la plupart des serveurs coupent l'URL entre 2 000 et 8 000 caractères. Un filtre JSON profond dépasse vite.
  • Encodage pénible : chaque caractère spécial doit être URL-encodé, les structures imbriquées deviennent illisibles.
  • Fuite de données : l'URL est journalisée partout — logs serveur, historique navigateur, analytics, Referer. Des critères sensibles y transitent en clair.

Le mensonge sémantique de POST

Face à ce mur, l'industrie a bricolé la solution évidente : POST /search avec un corps JSON. Ça marche, mais c'est un abus. POST signifie « traite ce contenu pour créer ou modifier une ressource ». L'utiliser pour une simple lecture casse trois garanties :

  • Le cache HTTP ne s'applique pas : chaque recherche identique refrappe le serveur.
  • L'idempotence n'est pas garantie : un proxy ou un client ne peut pas rejouer la requête en toute sécurité.
  • La cohérence de l'API se brouille : POST pour lire, POST pour écrire — impossible de raisonner sur le trafic.
Note : ce compromis dure depuis plus de quinze ans. GraphQL a d'ailleurs contourné le problème en faisant tout passer par POST. QUERY répond enfin proprement au besoin, au niveau du protocole.

QUERY en bref : ce que dit la RFC 10008

En bref : la RFC 10008 (publiée en juin 2026) définit QUERY comme une méthode qui « demande à la cible de traiter le contenu fourni de manière sûre et idempotente, puis de répondre avec le résultat de ce traitement ». Sûre, idempotente, cacheable — avec un corps de requête.

La définition officielle tient en une phrase, mais chaque mot compte. Décortiquons les trois propriétés fondamentales que QUERY apporte, là où GET et POST échouaient.

Propriété Signification Bénéfice concret
Sûre (safe) La requête ne demande aucune modification d'état côté serveur. Les crawlers, préchargeurs et proxies peuvent l'exécuter sans risque.
Idempotente La rejouer N fois produit le même effet que l'exécuter une fois. Retry automatique possible en cas de timeout réseau.
Cacheable La réponse peut être mise en cache — mais la clé inclut le corps. Deux recherches identiques servent la même réponse cachée.

La subtilité, on y reviendra, tient à la dernière ligne : contrairement à GET dont la clé de cache est l'URI, la clé de cache d'une réponse QUERY doit intégrer le contenu du corps. C'est ce qui rend la fonctionnalité puissante… et délicate à implémenter.

Anatomie d'une requête QUERY

En bref : une requête QUERY ressemble à un POST avec un corps JSON, mais impose un Content-Type (sinon 400). La réponse peut renvoyer Content-Location et Location pour rendre la recherche partageable.

Voici une requête QUERY complète, telle qu'elle circule sur le fil :

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

{
  "filters": { "category": "keyboards", "inStock": true },
  "sort": [{ "field": "price", "order": "asc" }],
  "page": { "size": 20 }
}

Les en-têtes qui comptent

  • Content-Type (obligatoire) : décrit le format du corps. En son absence, le serveur doit répondre 400 Bad Request.
  • Accept : indique le format de réponse souhaité (JSON, CSV, etc.).
  • Content-Location (réponse) : pointe vers une ressource représentant le résultat de la requête.
  • Location (réponse) : pointe vers une ressource représentant la requête elle-même, ce qui permet de générer un lien partageable vers une recherche.

Les codes d'erreur spécifiques

Code Quand ?
400 Bad RequestEn-tête Content-Type absent.
415 Unsupported Media TypeLe Content-Type fourni n'est pas géré par l'endpoint.
422 Unprocessable EntityLe corps est bien parsé, mais ne constitue pas une requête valide.

Une réponse type ressemble alors à ceci :

HTTP/1.1 200 OK
Content-Type: application/json
Content-Location: /products/results/9f2c1a
Location: /queries/9f2c1a
Cache-Control: max-age=60

{
  "items": [ { "id": 42, "name": "Keychron K8", "price": 89.9 } ],
  "total": 137,
  "page": { "size": 20, "next": "eyJvZmZzZXQiOjIwfQ==" }
}

QUERY vs GET vs POST

En bref : QUERY coche les trois cases de GET (sûre, idempotente, cacheable) tout en acceptant un corps comme POST. C'est littéralement l'union des deux, sans leurs défauts respectifs.
Méthode Sûre Idempotente Cacheable Corps de requête
GET ❌ (sémantique indéfinie)
POST
QUERY

Retenez la règle de décision suivante pour vos futurs endpoints :

  • Lecture simple, peu de paramètres → GET (rien ne change).
  • Lecture complexe avec corps structuré → QUERY (le nouveau standard cible).
  • Création ou modification de ressource → POST / PUT / PATCH.
Note : QUERY ne remplace pas GET. Pour récupérer une ressource unique par son identifiant (GET /products/42), GET reste parfait. QUERY vise le cas précis de la recherche paramétrée.

Appeler QUERY côté client

En bref : tout client HTTP acceptant un nom de méthode arbitraire peut déjà envoyer un QUERY : curl, fetch, axios. Le piège numéro un : écrire le nom en MAJUSCULES.

Avec curl

# -X QUERY force la méthode ; --data-raw porte le corps JSON
curl -X QUERY https://example.com/products \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  --data-raw '{
    "filters": { "category": "keyboards", "inStock": true },
    "sort": [{ "field": "price", "order": "asc" }],
    "page": { "size": 20 }
  }'

Avec fetch (le piège de la casse)

L'API Fetch ne normalise la casse que pour les méthodes standard (get, post, put…). Un nom non standard part verbatim. Écrivez "query" en minuscules et un serveur sensible à la casse rejettera la requête.

// ✅ CORRECT : "QUERY" en majuscules
const res = await fetch("/products", {
  method: "QUERY",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    filters: { category: "keyboards", inStock: true },
    sort: [{ field: "price", order: "asc" }],
    page: { size: 20 },
  }),
});

const data = await res.json();
console.log(`${data.total} produits trouvés`);

// ❌ PIÈGE : "query" minuscule part tel quel → rejet possible (405/501)
// fetch("/products", { method: "query", ... });

Avec axios

import axios from "axios";

// axios accepte n'importe quelle méthode via l'option `method`
const { data } = await axios.request({
  url: "/products",
  method: "QUERY",
  headers: { "Content-Type": "application/json" },
  data: {
    filters: { category: "keyboards", inStock: true },
    page: { size: 20 },
  },
});

console.log(data.items);

Un wrapper réutilisable avec fallback

En attendant que toute votre chaîne accepte QUERY, encapsulez l'appel pour retomber automatiquement sur POST si le serveur renvoie 405 Method Not Allowed :

// Client de recherche tolérant : QUERY d'abord, POST en secours
async function search(url, queryBody) {
  const options = {
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(queryBody),
  };

  // 1) Tenter la méthode moderne
  let res = await fetch(url, { method: "QUERY", ...options });

  // 2) Fallback si l'infra ne connaît pas encore QUERY
  if (res.status === 405 || res.status === 501) {
    console.warn("QUERY non supporté, bascule sur POST");
    res = await fetch(url, { method: "POST", ...options });
  }

  if (!res.ok) {
    throw new Error(`Recherche échouée : ${res.status}`);
  }
  return res.json();
}

Implémenter QUERY côté serveur

En bref : aucun framework n'a encore de helper natif router.query() généralisé. On enregistre QUERY comme une méthode custom, on valide le Content-Type, puis on traite le corps comme une lecture.

Node.js avec Express

Express route par méthode. Comme QUERY n'est pas encore un helper dédié, on utilise app.all() et on filtre sur req.method, ou l'API bas niveau router.route() :

import express from "express";

const app = express();
app.use(express.json()); // parse le corps JSON de la requête QUERY

// Endpoint de recherche via QUERY
app.all("/products", (req, res) => {
  // On ne gère que la méthode QUERY sur cette route
  if (req.method !== "QUERY") {
    res.set("Allow", "QUERY");
    return res.status(405).json({ error: "Méthode non autorisée" });
  }

  // 1) Content-Type obligatoire (RFC 10008 → 400 sinon)
  if (!req.is("application/json")) {
    return res.status(400).json({ error: "Content-Type application/json requis" });
  }

  const { filters = {}, sort = [], page = {} } = req.body ?? {};

  // 2) Valider la structure de la requête (sinon 422)
  if (typeof filters !== "object") {
    return res.status(422).json({ error: "Le champ 'filters' est invalide" });
  }

  // 3) Traiter la recherche (sûre + idempotente : aucune écriture)
  const results = runSearch(filters, sort, page);

  // 4) En-têtes de cache + ressource partageable
  res.set("Cache-Control", "max-age=60");
  res.set("Content-Location", `/products/results/${results.hash}`);
  return res.status(200).json(results.payload);
});

app.listen(3000, () => console.log("API QUERY prête sur :3000"));

ASP.NET Core (C#)

Avec ASP.NET Core, on déclare un attribut de routage custom via [AcceptVerbs] qui accepte n'importe quelle méthode, y compris QUERY :

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("products")]
public class ProductsController : ControllerBase
{
    // Autorise explicitement le verbe QUERY sur cette action
    [AcceptVerbs("QUERY")]
    public IActionResult Search([FromBody] ProductQuery query)
    {
        // ModelState valide le corps désérialisé (422 si invalide)
        if (!ModelState.IsValid)
            return UnprocessableEntity(ModelState);

        var results = _searchService.Run(query);

        // Réponse cacheable + lien vers le résultat
        Response.Headers["Cache-Control"] = "max-age=60";
        Response.Headers["Content-Location"] = $"/products/results/{results.Hash}";

        return Ok(results.Payload);
    }
}

// DTO de la requête (validation par DataAnnotations)
public class ProductQuery
{
    public Dictionary<string, object> Filters { get; set; } = new();
    public List<SortRule> Sort { get; set; } = new();
    public PageRule Page { get; set; } = new();
}

Python avec FastAPI

FastAPI s'appuie sur Starlette : on ajoute une route bas niveau qui déclare la méthode QUERY dans la liste methods :

from fastapi import FastAPI, Request, Response
from pydantic import BaseModel, ValidationError

app = FastAPI()

class ProductQuery(BaseModel):
    filters: dict = {}
    sort: list = []
    page: dict = {}

async def search_products(request: Request):
    # Content-Type obligatoire → 400 sinon
    if request.headers.get("content-type") != "application/json":
        return Response('{"error":"Content-Type requis"}', status_code=400,
                        media_type="application/json")
    try:
        body = ProductQuery(**(await request.json()))
    except ValidationError:
        return Response('{"error":"Requête invalide"}', status_code=422,
                        media_type="application/json")

    results = run_search(body)  # lecture sûre et idempotente
    return Response(results.json(), media_type="application/json",
                    headers={"Cache-Control": "max-age=60"})

# Enregistre la route avec la méthode custom QUERY
app.add_api_route("/products", search_products, methods=["QUERY"])

Intégration Angular avec HttpClient

En bref : HttpClient n'a pas de méthode query(), mais request() accepte n'importe quel verbe. On l'encapsule dans un service typé, avec repli sur POST.

La méthode générique HttpClient.request() prend le nom du verbe HTTP en premier argument. On peut donc envoyer un QUERY proprement et rester typé :

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, catchError, throwError } from 'rxjs';

export interface ProductQuery {
  filters?: Record<string, unknown>;
  sort?: { field: string; order: 'asc' | 'desc' }[];
  page?: { size: number };
}

export interface ProductResult {
  items: Product[];
  total: number;
}

@Injectable({ providedIn: 'root' })
export class ProductSearchService {
  private readonly url = '/products';

  constructor(private http: HttpClient) {}

  // Envoie un QUERY via la méthode générique request()
  search(query: ProductQuery): Observable<ProductResult> {
    return this.http
      .request<ProductResult>('QUERY', this.url, {
        body: query,
        headers: { 'Content-Type': 'application/json' },
      })
      .pipe(
        catchError((err) => {
          // Repli sur POST si l'infra ne connaît pas QUERY
          if (err.status === 405 || err.status === 501) {
            return this.http.post<ProductResult>(this.url, query);
          }
          return throwError(() => err);
        })
      );
  }
}

Côté composant, l'usage reste identique à n'importe quel appel HttpClient :

import { Component, inject, signal } from '@angular/core';
import { ProductSearchService } from './product-search.service';

@Component({ selector: 'app-search', standalone: true, template: '...' })
export class SearchComponent {
  private search = inject(ProductSearchService);
  results = signal<Product[]>([]);

  onSearch(): void {
    this.search
      .search({ filters: { category: 'keyboards', inStock: true }, page: { size: 20 } })
      .subscribe((res) => this.results.set(res.items));
  }
}

Cache, CORS et proxies : les pièges de production

En bref : le code applicatif est la partie facile. Le vrai travail, c'est autoriser QUERY à traverser CORS, Nginx, les WAF et les API gateways — sans quoi la requête meurt avant d'atteindre votre serveur.

CORS : un preflight obligatoire

QUERY n'est pas une méthode « CORS-safelisted ». Tout appel cross-origin déclenche donc une requête preflight OPTIONS. Le serveur doit répondre en autorisant explicitement le verbe :

// Middleware CORS Express : autoriser QUERY dans le preflight
app.use((req, res, next) => {
  res.header("Access-Control-Allow-Origin", "https://app.example.com");
  // Sans QUERY dans cette liste, le navigateur bloque la requête réelle
  res.header("Access-Control-Allow-Methods", "GET, POST, QUERY, OPTIONS");
  res.header("Access-Control-Allow-Headers", "Content-Type, Accept");

  if (req.method === "OPTIONS") return res.sendStatus(204);
  next();
});

Nginx : ne pas filtrer la méthode

Beaucoup de configurations Nginx utilisent limit_except pour n'autoriser que GET et POST. Un tel bloc renverra 405 sur un QUERY. Il faut l'ajouter à la liste blanche :

location /products {
    # Autoriser explicitement QUERY (sinon 405 avant d'atteindre l'app)
    limit_except GET POST QUERY {
        deny all;
    }
    proxy_pass http://backend_api;
    proxy_set_header Host $host;
}

Le cache : la clé inclut le corps

C'est le point le plus subtil. Pour GET, la clé de cache est l'URI. Pour QUERY, deux requêtes vers /products peuvent avoir des corps différents et donc des résultats différents. La clé de cache doit intégrer un hash normalisé du corps :

import { createHash } from "node:crypto";

// Normaliser le corps (tri des clés) puis le hasher pour la clé de cache
function cacheKey(url, body) {
  const normalized = JSON.stringify(sortKeysDeep(body));
  const hash = createHash("sha256").update(normalized).digest("hex").slice(0, 16);
  return `query:${url}:${hash}`;
}

// Deux recherches équivalentes → même clé → même réponse cachée
const key = cacheKey("/products", { inStock: true, category: "keyboards" });

Pagination : pas de Range HTTP

La RFC déconseille les requêtes Range HTTP pour paginer une réponse QUERY. Le format de requête doit porter sa propre pagination (curseur, offset/limit, ou l'équivalent SQL FETCH FIRST), comme dans nos exemples avec le champ page.

Sécurité : un endpoint de recherche est une surface d'attaque

Accepter un corps structuré et arbitraire ouvre la porte à des abus classiques. Une recherche est certes « sûre » au sens HTTP (elle ne modifie rien), mais elle peut être coûteuse : un attaquant peut forger un corps qui déclenche des requêtes SQL lourdes ou une consommation mémoire massive. Trois garde-fous s'imposent.

  • Limiter la taille du corps pour éviter les payloads géants (déni de service).
  • Valider strictement la structure (allow-list de champs et d'opérateurs) — ne jamais interpréter le corps comme du code ou une requête brute.
  • Appliquer un rate limiting, comme sur toute API publique.
import rateLimit from "express-rate-limit";

// 1) Plafonner la taille du corps JSON (anti-DoS)
app.use("/products", express.json({ limit: "16kb" }));

// 2) Limiter le débit par IP sur l'endpoint de recherche
const searchLimiter = rateLimit({ windowMs: 60_000, max: 60 });
app.use("/products", searchLimiter);

// 3) Allow-list stricte : seuls ces champs sont acceptés dans le corps
const ALLOWED_FILTERS = new Set(["category", "inStock", "minPrice", "maxPrice"]);

function sanitizeFilters(filters) {
  return Object.fromEntries(
    Object.entries(filters).filter(([key]) => ALLOWED_FILTERS.has(key))
  );
}
Note : comme toujours, gardez les secrets (clés de connexion BDD, tokens) dans des variables d'environnement, jamais dans le code ni dans le corps de la requête.

Stratégie d'adoption progressive

En bref : ne réécrivez pas vos POST /search existants du jour au lendemain. QUERY brille surtout sur les nouveaux endpoints, avec un fallback POST tant que l'écosystème mûrit.

Une adoption raisonnable suit trois temps :

  1. Nouveaux endpoints d'abord. Concevez vos nouvelles routes de recherche en QUERY, avec un repli POST transparent côté client (voir nos wrappers plus haut).
  2. Vérifiez toute la chaîne. Avant d'exposer publiquement, testez que CORS, Nginx, le CDN, le WAF et l'API gateway laissent bien passer la méthode. Une seule couche récalcitrante casse tout.
  3. Ne migrez l'existant que si le gain est réel. Un POST /search qui fonctionne n'a pas besoin d'être réécrit « pour la pureté ». Migrez quand le cache HTTP ou l'idempotence vous apportent un bénéfice mesurable.

✅ Checklist avant de servir QUERY en production

  • Nom de méthode envoyé en MAJUSCULES côté client
  • Content-Type validé côté serveur (400 sinon)
  • Corps validé structurellement (422 sinon)
  • Traitement strictement en lecture (sûr + idempotent)
  • Preflight CORS autorisant QUERY
  • Nginx / proxies / WAF configurés pour laisser passer
  • Clé de cache incluant un hash normalisé du corps
  • Pagination portée par le format, pas par Range
  • Fallback POST implémenté côté client

Conclusion

La méthode QUERY corrige une frustration vieille de quinze ans : elle réunit enfin le corps de requête riche de POST et la sémantique sûre, idempotente et cacheable de GET. Pour les endpoints de recherche complexe, c'est le chaînon manquant du REST — plus honnête, plus cohérent, mieux caché.

Mais restons mesurés sur l'impact à court terme. Une RFC ne rend pas une méthode utilisable du jour au lendemain. En 2026, l'écosystème n'est pas prêt : les navigateurs, CDN, WAF, API gateways et frameworks doivent encore la reconnaître et l'autoriser. Une requête QUERY non prévue se fait souvent rejeter par un intermédiaire bien avant d'atteindre votre code. GraphQL occupe déjà une partie du terrain, et le support natif dans les frameworks s'étalera sur plusieurs années.

Le bon réflexe aujourd'hui n'est donc pas de remplacer tous vos GET et POST, mais de connaître la méthode, l'expérimenter en interne, et l'adopter progressivement sur vos nouveaux endpoints avec un fallback. C'est une évolution lente mais saine du protocole HTTP — du même acabit que l'arrivée de PATCH. À moyen terme, QUERY deviendra le moyen canonique de faire des recherches structurées en REST. À court terme, c'est avant tout un excellent sujet de veille à garder à l'œil.

🎯 À retenir :
  • Ce que c'est : RFC 10008 — méthode HTTP sûre, idempotente, cacheable, avec corps.
  • À quoi ça sert : recherches et filtres complexes, sans abuser de POST.
  • Utilisable ? Côté client oui ; côté infra, prévoir un fallback POST.
  • Verdict : vraie bonne nouvelle sur le fond, adoption réelle à 2-3 ans.

Partager