🎯 OBJECTIF
Comprendre comment :
safe, idempotent et cacheable définissent la sémantique d'une méthode HTTP, indépendamment de ce que fait ton codeAccept-Query, Location vs Content-Location, requêtes conditionnelles, cache key incluant le bodyPOST /search existant sans casser tes clients🧠 MODÈLE MENTAL
Un verbe HTTP n'est pas un nom de fonction. C'est un contrat annoncé au réseau. Entre ton client et ton serveur il y a un proxy, un CDN, un WAF, un load balancer, une lib de retry : aucun ne lit ton code, tous lisent la méthode. Quand tu écris POST /search, tu dis à toute cette chaîne « ceci peut modifier l'état, ne rejoue rien, ne cache rien ». Le CDN obéit, la lib de retry obéit, et tu perds gratuitement le cache et les retries sur une opération qui ne fait que lire.
Le problème est vieux comme REST. GET porte la bonne sémantique mais ne peut transporter ses paramètres que dans l'URI, avec des limites de taille que personne ne connaît vraiment (la chaîne traverse des systèmes non coordonnés), un encodage pénible pour les filtres imbriqués, et des URI qui finissent dans les logs et les favoris. POST porte un body arbitrairement gros mais renonce à tout ce que GET garantit.
QUERY est la réponse officielle : un verbe explicitement safe et idempotent, qui attend un body, dont la réponse est cacheable. C'est le premier verbe HTTP généraliste ajouté depuis PATCH en 2010. La nuance qui compte : le protocole est standardisé, l'écosystème ne l'est pas. Mi-2026 la question n'est plus « est-ce que c'est propre » mais « est-ce que mon routeur, mon proxy et mon WAF savent l'épeler ».
ETag ou Last-Modified, l'empreinte d'une représentation qui rend possibles les requêtes conditionnelles.Accept-Query en est un, contrairement à Accept.POST /search, POST /users/42/delete). C'est exactement ce que QUERY vient supprimer côté lecture.Tout le reste découle de là. Un verbe n'est pas « lire » ou « écrire », c'est un point dans un espace à trois dimensions, plus une quatrième question pratique : est-ce que le body de requête a un sens ?
Un GET qui incrémente un compteur de vues reste safe : la ressource cible n'a pas changé du point de vue du client. Un GET qui supprime une commande est un bug de conception, pas une optimisation. C'est cette propriété qui autorise un crawler ou un préfetcher de navigateur à appeler l'endpoint sans te demander la permission.
La propriété la plus mal comprise, parce qu'on la confond avec « même réponse ».
| Cas | Réponses successives | État final | Idempotent ? |
|---|---|---|---|
DELETE /orders/42 deux fois |
204 puis 404 | identique | ✅ oui |
PUT /orders/42 avec le même body |
200 puis 200 | identique | ✅ oui |
PATCH avec {"op":"replace","path":"/stock","value":12} |
200 puis 200 | identique | ✅ oui |
PATCH avec {"op":"increment","path":"/stock","value":1} |
200 puis 200 | différent | ❌ non |
POST /orders |
201 puis 201 | deux commandes | ❌ non |
La ligne 1 est le contre-exemple utile : deux codes différents, une seule sémantique. La ligne 4 est l'inverse, et c'est celle qui casse en prod après un timeout réseau.
Cacheable dépend du verbe et des en-têtes de fraîcheur. POST est techniquement cacheable, mais seulement pour resservir la réponse à un futur GET ou HEAD sur la même URI, avec des informations de fraîcheur explicites. En pratique aucun cache ne le fait, ce qui revient à « POST n'est pas cachable ».
Une requête ne va jamais directement du client à ton serveur. Elle traverse un CDN, un WAF, un load balancer, parfois un proxy d'entreprise, et aucun de ces composants ne connaît ton domaine métier : ils prennent leurs décisions sur le verbe et les en-têtes.
Le diagramme répond à une seule question : qui décide quoi, le long du chemin, en fonction du verbe ?
flowchart LR
subgraph P["POST /search — lecture déguisée"]
direction TB
C1[Client] -->|"pas de retry auto"| R1[Lib HTTP]
R1 -->|"méthode non safe :<br/>passe sans cacher"| CDN1[CDN]
CDN1 -->|"traité comme<br/>une écriture"| W1[WAF]
W1 --> S1["Origine<br/>recalcul systématique"]
end
subgraph Q["QUERY /search — lecture déclarée"]
direction TB
C2[Client] -->|"retry autorisé"| R2[Lib HTTP]
R2 -->|"safe + idempotent :<br/>cache possible"| CDN2[CDN]
CDN2 -->|"méthode à autoriser<br/>explicitement"| W2[WAF]
W2 --> S2["Origine<br/>+ Location vers requête<br/>rejouable en GET"]
end
style S1 fill:#f59e0b,stroke:#b45309,color:#fff
style S2 fill:#0ea5e9,stroke:#0369a1,color:#fffmermaid⚠️ Le piège le plus courant
Idempotence ≠ même code de réponse. Idempotence ≠ concurrence gérée.
Deux PUT simultanés avec des payloads différents sont chacun idempotents et produisent quand même un last-write-wins non déterministe. Pour la concurrence il faut des requêtes conditionnelles (If-Match + ETag), pas un verbe.
🔑 Conclusion clé
Le verbe est lu par des intermédiaires qui ne connaissent rien à ton domaine. Choisir un verbe, c'est autoriser ou interdire à cette chaîne de cacher, rejouer, précharger et logger. C'est une décision d'infrastructure déguisée en décision de code.
safe·idempotent·cacheable· body sans sémantique définie
Récupère une représentation de la ressource identifiée par l'URI cible. Le verbe de lecture, point.
Un body sur GET n'est pas interdit, il n'a simplement aucune sémantique définie. Résultat pratique : certaines libs le suppriment silencieusement, certains proxies rejettent la requête, certains serveurs l'ignorent. Envoyer un body en GET, c'est bâtir sur du comportement non spécifié.
⚠️ La limite de taille d'URI n'existe pas
La RFC 9110 recommande de supporter au moins 8000 octets de request line. Ce n'est ni une garantie côté serveur, ni une limite haute réelle. Les limites observées vont de 2 Ko à 8 Ko selon les intermédiaires, et tu ne connais pas la chaîne complète que ta requête traverse. C'est exactement le problème que QUERY vient résoudre.
safe·idempotent·cacheable· pas de body de réponse
Identique à GET, sans le body en réponse. Utile pour vérifier existence, taille ou validateurs sans transférer la ressource.
Les en-têtes doivent être ceux qu'aurait renvoyés le GET équivalent, ce qui est une source d'incohérences classique quand les frameworks calculent Content-Length paresseusement.
non safe·non idempotent· cacheable en théorie seulement · body attendu
Demande à la ressource cible de traiter le contenu fourni selon sa propre sémantique. C'est le verbe « tout le reste », ce qui est à la fois sa force et son abus le plus fréquent : création, commande métier, appel RPC, upload, recherche.
Pour rendre un POST rejouable sans doublon, la pratique est une clé d'idempotence fournie par le client, que le serveur stocke avec le résultat :
POST /payments HTTP/1.1
Host: example.org
Content-Type: application/json
Idempotency-Key: 7f3a9c12-4d5e-4f61-9b02-8ac1e5d4b7f0
{ "amount": 4200, "currency": "EUR", "order": "ORD-88213" }
⚠️ Une clé d'idempotence n'est pas de l'idempotence
Elle est applicative. Le protocole continue d'annoncer « non idempotent », donc aucun intermédiaire ne rejouera pour toi : c'est ton client qui doit retenter, et ton serveur qui doit dédupliquer. Tu achètes la sécurité du rejeu, pas son automatisme.
Le même raisonnement s'applique côté messages, où l'idempotence ne peut pas non plus venir du transport : voir pattern-outbox-publication-fiable-de-messages-depuis-une-transaction-db.
non safe·idempotent· non cacheable · représentation complète
Remplace intégralement la représentation de la ressource cible par le contenu fourni. Deux propriétés le distinguent de POST :
PUT /documents/{uuid})Codes usuels : 201 si la ressource est créée, 200 ou 204 si elle est remplacée.
⚠️ Un champ absent d'un PUT est un champ supprimé
PUT /users/42 HTTP/1.1
Content-Type: application/json
{ "name": "Camille Dubois", "email": "camille@example.net" }
Si la ressource portait aussi phone et locale, ils n'existent plus. C'est le bug le plus fréquent des clients qui envoient un formulaire partiel en PUT : ils écrasent ce qu'ils n'affichaient même pas.
Le correctif n'est pas de « tolérer » les champs absents côté serveur, ce qui transforme ton PUT en PATCH déguisé et lui fait perdre la sémantique qu'il annonce. C'est d'exposer un vrai PATCH.
non safe·non idempotentpar défaut · non cacheable · document de patch
Applique une modification partielle décrite par le body. Défini par la RFC 5789, donc plus jeune que le cœur d'HTTP.
Le body de PATCH n'est pas la ressource, c'est un document de patch. Deux formats normalisés dominent, et le choix n'est pas cosmétique.
PATCH /users/42 HTTP/1.1
Content-Type: application/json-patch+json
[
{ "op": "test", "path": "/version", "value": 7 },
{ "op": "replace", "path": "/email", "value": "c.dubois@example.net" },
{ "op": "remove", "path": "/phone" },
{ "op": "add", "path": "/tags/-", "value": "vip" }
]
PATCH /users/42 HTTP/1.1
Content-Type: application/merge-patch+json
{ "email": "c.dubois@example.net", "phone": null }
| Critère | application/json-patch+json (RFC 6902) |
application/merge-patch+json (RFC 7396) |
|---|---|---|
| Forme | liste d'opérations ordonnées | objet partiel fusionné récursivement |
| Suppression | {"op":"remove"} |
valeur null |
| Élément de tableau | ✅ ciblable par index ou - |
❌ le tableau est remplacé en entier |
| Pré-condition dans le patch | ✅ via op: test (optimistic locking) |
❌ aucune |
| Lisibilité | verbeuse | immédiate |
Stocker null comme valeur |
✅ possible | ❌ impossible, null est le marqueur de suppression |
L'en-tête de réponse Accept-Patch annonce les formats acceptés, exactement comme Accept-Query le fera pour QUERY.
✅ Rendre un PATCH idempotent
C'est un choix de format, pas une fatalité du verbe :
replace d'une valeur fixe, remove) → idempotentadd en fin de tableau) → non idempotentSi tu as besoin d'un incrément rejouable, la voie propre est un replace sur une valeur calculée par le client, avec op: test ou If-Match pour détecter le conflit.
non safe·idempotent· non cacheable · body inhabituel
Demande la suppression de l'association entre l'URI cible et sa ressource. Codes usuels : 204 sans body, 200 avec body de confirmation, 202 si la suppression est asynchrone.
✅ Le 404 au second DELETE est correct
Un second appel qui renvoie 404 n'entame pas l'idempotence : l'état serveur final est le même. C'est même préférable à un 204 mensonger si tu as une distinction utile entre « supprimé » et « n'a jamais existé ». L'idempotence porte sur l'état, jamais sur le code.
safe·idempotent· non cacheable
Interroge les capacités de communication pour une ressource. Deux usages réels : l'en-tête Allow qui liste les méthodes supportées (le mécanisme de découverte le plus simple pour QUERY, voir §4), et le preflight CORS déclenché par les navigateurs.
safe·idempotent· non cacheable
Boucle de diagnostic : le serveur renvoie la requête reçue.
⚠️ À désactiver en production
Combiné à une faille XSS, TRACE ouvre la porte au Cross-Site Tracing : le JavaScript de l'attaquant fait renvoyer par le serveur des en-têtes normalement inaccessibles depuis le navigateur (cookies HttpOnly, en-têtes d'authentification).
Le verbe est safe au sens HTTP et dangereux au sens sécurité. Les deux notions n'ont aucun rapport.
non safe·non idempotent· non cacheable
Établit un tunnel vers le serveur identifié par la cible. C'est de la plomberie de proxy (TLS via un proxy HTTP), pas un verbe d'API.
Publié le 15 juin 2026 comme Proposed Standard sur le Standards Track de l'IETF, par Julian Reschke (greenbytes), James M. Snell (Cloudflare) et Mike Bishop (Akamai). Il vient du draft draft-ietf-httpbis-safe-method-w-body, en discussion depuis plus de dix ans dans le groupe HTTPBIS. C'est le premier verbe HTTP généraliste depuis PATCH (RFC 5789, mars 2010), soit un écart de seize ans.
safe·idempotent·cacheable· body attendu,Content-Typeobligatoire
QUERY demande à la ressource cible d'exécuter une requête de façon safe et idempotente, en décrivant cette requête dans le body, et de répondre avec le résultat du traitement. Le body plus son media type définissent la requête ; la ressource cible définit le périmètre interrogé.
QUERY /articles HTTP/1.1
Host: example.org
Content-Type: application/json
Accept: application/json
{
"filters": {
"tags": { "all": ["http", "api-design"] },
"published": { "gte": "2026-01-01" },
"author": { "in": ["reschke", "snell", "bishop"] }
},
"sort": [{ "field": "published", "order": "desc" }],
"limit": 50
}
Le positionnement, tel que le RFC le résume lui-même :
| Propriété | GET | QUERY | POST |
|---|---|---|---|
| Safe | ✅ oui | ✅ oui | ⚠️ potentiellement non |
| Idempotent | ✅ oui | ✅ oui | ⚠️ potentiellement non |
| URI pour la requête elle-même | ✅ oui, par définition | ➖ optionnelle (Location) |
❌ non |
| URI pour le résultat | ➖ optionnelle (Content-Location) |
➖ optionnelle (Content-Location) |
➖ optionnelle (Content-Location) |
| Cacheable | ✅ oui | ✅ oui | ⚠️ seulement pour de futurs GET/HEAD |
| Body de requête | ❌ « aucune sémantique définie » | ✅ attendu | ✅ attendu |
Le serveur doit rejeter la requête si Content-Type est absent ou incohérent avec le body. Le RFC va jusqu'à interdire explicitement de deviner le type à partir du contenu pour corriger une valeur manquante ou fausse.
| Situation | Code | À renvoyer avec |
|---|---|---|
| Pas de media type | 400 Bad Request | |
| Media type non supporté, ou connu mais sans sémantique QUERY définie | 415 Unsupported Media Type | Accept-Query ou Accept |
| Media type déclaré incohérent avec le body réel | 400 Bad Request | |
| Body syntaxiquement valide, requête inexécutable (table inexistante, champ inconnu) | 422 Unprocessable Content | détail de l'erreur |
Accept demandé non supporté en réponse |
406 Not Acceptable | les types disponibles |
| Méthode inconnue de la ressource | 405 Method Not Allowed | Allow |
✅ La distinction 415 / 422 vaut le détour
« Je ne parle pas ce langage de requête » (415) et « je le parle, mais ta requête n'a pas de sens » (422) sont deux erreurs différentes, actionnables différemment côté client. La plupart des POST /search existants les écrasent en un seul 400 générique. C'est un gain de design que tu peux appliquer avant même de migrer vers QUERY.
Deux mécanismes complémentaires, plus une option opportuniste.
1. Le verbe existe-t-il ?
OPTIONS /articles HTTP/1.1
Host: example.org
HTTP/1.1 200 OK
Allow: GET, HEAD, OPTIONS, QUERY
2. Dans quel langage interroger ? C'est le rôle du nouvel en-tête de réponse Accept-Query :
HTTP/1.1 200 OK
Content-Type: application/json
Accept-Query: "application/jsonpath", application/sql;charset="UTF-8"
Points de spec à ne pas rater :
Accept. Tokens et Strings sont sémantiquement équivalents : un receveur ne doit pas se comporter différemment selon la forme reçue.*/* et type/*.3. La voie pragmatique. Envoyer le QUERY sans rien savoir : méthode inconnue → 405 avec Allow, format inconnu → 415 avec Accept. Dans les deux cas tu apprends ce qu'il faut, au prix d'un aller-retour.
Les deux en-têtes existaient déjà, mais QUERY leur donne un rôle précis, et les confondre est l'erreur de conception la plus probable.
| En-tête | Pointe vers | Un GET dessus renvoie | Durée de vie |
|---|---|---|---|
Content-Location |
le résultat que tu viens de recevoir | les mêmes données, figées | peut être temporaire |
Location |
la ressource équivalente, la requête elle-même | des résultats réactualisés | peut être temporaire |
HTTP/1.1 200 OK
Content-Type: application/json
Content-Location: /articles/results/8f21
Location: /articles/saved-queries/443
ETag: "443-7"
Ici /articles/results/8f21 est un instantané figé, /articles/saved-queries/443 est vivant. C'est la seconde qui a de la valeur opérationnelle : elle permet au client de quitter QUERY pour GET sur les appels suivants, donc de retrouver du cache trivial et des requêtes conditionnelles sans jamais renvoyer le body.
Le serveur peut aussi ne rien renvoyer du tout et répondre 303 See Other avec un Location : il annonce que la requête se récupère par un GET normal sur cette URI, sans produire le résultat dans cette réponse.
| Statut | Comportement attendu du client |
|---|---|
| 301, 308 | rejouer un QUERY vers la nouvelle URI (déplacement permanent) |
| 302, 307 | rejouer un QUERY vers la nouvelle URI (déplacement temporaire) |
| 303 | faire un GET sur l'URI de Location |
⚠️ Le piège d'implémentation des clients HTTP
La tolérance historique qui autorise à transformer un POST en GET après un 301 ou 302 ne concerne pas QUERY. Sauf que beaucoup de clients HTTP appliquent cette transformation en dur pour tout ce qui n'est pas GET : ton body disparaît silencieusement et le serveur reçoit un GET vide.
À tester explicitement dans ta lib avant de mettre QUERY derrière un domaine qui redirige (http vers https, apex vers www, ancien host vers nouveau).
La représentation sélectionnée d'un QUERY est celle qu'aurait renvoyée un GET sur la ressource équivalente. Concrètement, If-None-Match et If-Modified-Since fonctionnent sur QUERY, et un 304 Not Modified est une réponse valide à un QUERY conditionnel : tu renvoies ton body de requête, le serveur te répond « rien de neuf » sans recalculer ni retransmettre les résultats.
Le diagramme répond à la question : comment enchaîner QUERY et GET pour ne payer le body qu'une seule fois ?
sequenceDiagram
autonumber
participant C as Client
participant R as Ressource /articles (QUERY)
participant E as Ressource équivalente /saved-queries/443
C->>R: QUERY /articles + body (filtres JSON)
R->>E: crée (ou réutilise) l'URI équivalente
R-->>C: 200 OK · ETag "443-7" · Location /saved-queries/443
Note over C: le client mémorise l'URI et le validateur<br/>le body ne sera plus jamais renvoyé
C->>E: GET /saved-queries/443 · If-None-Match "443-7"
E-->>C: 304 Not Modified (aucun body réémis)
Note over E: le jeu de données change
C->>E: GET /saved-queries/443 · If-None-Match "443-7"
E-->>C: 200 OK · ETag "443-8" · résultats à jourmermaidLa réponse à un QUERY est cacheable et un cache peut la resservir à des QUERY ultérieurs. La contrainte structurante : la cache key doit intégrer le contenu de la requête et ses métadonnées associées.
Les caches sont autorisés à normaliser le body avant de calculer la clé, pour éviter que deux requêtes sémantiquement identiques produisent deux entrées :
+json)La transformation ne sert qu'au calcul de clé, elle ne modifie pas la requête transmise. Un client peut demander à ce que rien ne soit transformé via la directive no-transform, mais elle est seulement indicative.
⚠️ Le vrai coût du cache QUERY
Coût de traitement. Cacher un QUERY oblige le cache à lire tout le body avant de savoir quelle entrée consulter. Sur des filtres de plusieurs dizaines de kilooctets, cela déplace du travail vers un composant jusqu'ici optimisé pour hacher une URI.
Risque de corruption. Si un cache normalise le body différemment de la façon dont la ressource l'interprète, il produit des faux positifs : deux requêtes distinctes se voient servir la même réponse. C'est un cache poisoning silencieux, sans attaquant. Le RFC le signale explicitement dans ses considérations de sécurité.
Conséquence pratique. Le chemin robuste n'est pas « je cache mes QUERY », c'est « je renvoie Location et mes clients passent en GET ».
Sémantique identique à GET, et sans grand intérêt : découper un résultat de requête en plages d'octets n'a pas de sens métier. Le RFC dit clairement qu'il s'attend à voir la pagination gérée par le format de requête lui-même (FETCH FIRST ... ROWS ONLY en SQL, limit ou curseur dans un DSL JSON).
| Sujet | Ce qu'il faut retenir |
|---|---|
| Fuite par URI | Les URI sont plus souvent loggées, historisées et bookmarkées que les bodies. Des critères de recherche sensibles ont leur place dans un body, pas dans un access log. C'est l'argument principal en faveur de QUERY sur GET. |
| URI de résultat temporaire | Si le serveur crée une ressource pour porter le résultat, cette URI ne doit pas encoder les parties sensibles de la requête, sinon la fuite est simplement déplacée du log de requête vers le log d'URI. |
| CORS | QUERY n'est pas dans la safelist : tout appel cross-origin depuis un navigateur déclenche un preflight OPTIONS. Un aller-retour supplémentaire à budgéter. |
| CSRF | Beaucoup de protections raisonnent « GET est safe, tout le reste est suspect ». QUERY est safe et n'est pas GET : les règles à base de liste de méthodes doivent être relues, pas devinées. |
| WAF et middleboxes | Tout ce qui filtre par liste blanche de méthodes (limit_except nginx, règles WAF, ACL de gateway) rejettera QUERY jusqu'à ajout explicite. C'est là que se trouvera l'essentiel de la friction initiale. |
Le registre IANA contenait déjà trois méthodes safe et idempotentes : PROPFIND, REPORT et SEARCH. Les premières versions du draft utilisaient d'ailleurs SEARCH. Trois raisons ont fait basculer le choix :
application/xml), alors que QUERY fait du media type le porteur de la sémantiqueflowchart TD
A["Que fait l'opération ?"] --> B{"Modifie l'état<br/>de la ressource ?"}
B -->|Non, lecture seule| C{"Les paramètres tiennent<br/>dans l'URI ?"}
C -->|Oui| D["GET<br/>(HEAD si métadonnées seules)"]
C -->|"Non : filtres imbriqués,<br/>> ~2 Ko, données sensibles"| E{"QUERY supporté<br/>de bout en bout ?"}
E -->|Oui| F["QUERY"]
E -->|Non| G["POST + doc explicite<br/>+ Idempotency-Key si retries"]
B -->|Oui| H{"Le client connaît-il<br/>l'URI cible ?"}
H -->|"Oui, remplacement complet"| I["PUT"]
H -->|"Oui, modification partielle"| J["PATCH<br/>(json-patch ou merge-patch)"]
H -->|"Non, le serveur crée l'URI"| K["POST sur la collection"]
B -->|"Suppression"| L["DELETE"]
style F fill:#0ea5e9,stroke:#0369a1,color:#fff
style D fill:#10b981,stroke:#047857,color:#fff
style G fill:#f59e0b,stroke:#b45309,color:#fffmermaidLe seul nœud vraiment nouveau est celui du milieu : jusqu'à juin 2026 la branche « non » n'existait pas, tout tombait sur POST.
Le protocole est figé, l'adoption ne l'est pas. Un verbe aussi fondamental doit être reconnu couche par couche : runtime, framework, reverse proxy, gateway, CDN, outillage d'observabilité, navigateur.
| Couche | État constaté début juillet 2026 |
|---|---|
| Node.js | ✅ parse QUERY nativement depuis 21.7.2 et 22+, donc avant la publication du RFC |
| Fastify | ✅ exposé via addHttpMethod |
| Symfony | ✅ intégré depuis 7.4 (HttpFoundation traite le body QUERY comme celui d'un POST) |
| ASP.NET Core | 🟡 reconnu dans les previews .NET 11 |
| Spring Framework | ❌ PR ouverte, mais RequestMethod n'inclut pas encore QUERY : impossible de router via @RequestMapping (voir spring-boot-controllers pour le mapping actuel) |
| Rails | 🟡 proposition en discussion |
| Jetty / Tomcat | 🟡 tickets d'adoption ouverts |
| nginx | 🟡 prise en charge basique en upstream |
| Cloudflare, AWS ALB, Envoy | ✅ laissent passer vers l'origine ; les middleboxes anciennes restent à tester |
| OpenAPI | ✅ 3.2 sait modéliser une opération QUERY |
| Navigateurs | ❌ pas dans la safelist CORS, donc preflight obligatoire. L'état exact de fetch() est incertain : les sources publiées se contredisent, à vérifier soi-même avant de compter dessus. |
🔑 Conclusion clé
Mi-2026, QUERY est une histoire serveur à serveur et gateway, pas navigateur. Ce qui n'est pas gênant : la recherche lourde, le reporting et le trafic de requêtes internes vivent exactement là. Un premier déploiement raisonnable est le saut BFF vers service.
POST /search sans casser les clientslimit_except, ACL de service mesh, règles de CDN. Ajouter QUERY explicitement partout. C'est ici que ça casse, pas dans le code.Allow sur OPTIONS, Accept-Query avec les media types réellement supportés.Content-Type absent ou incohérent, distinguer 415 et 422. C'est obligatoire côté spec, et cela améliore aussi le POST existant.Location vers une requête sauvegardée dès que le pattern d'usage est répétitif (polling, dashboard, export). C'est ce qui convertit ton endpoint de recherche en ressource cachable en GET.| Verbe | Safe | Idempotent | Cacheable | Body requête | Usage canonique |
|---|---|---|---|---|---|
GET |
✅ | ✅ | ✅ | sans sémantique définie | lire une représentation |
HEAD |
✅ | ✅ | ✅ | non | métadonnées sans transfert |
QUERY |
✅ | ✅ | ✅ (clé = URI + body) | attendu, Content-Type requis |
lecture complexe ou volumineuse |
POST |
❌ | ❌ | en théorie seulement | attendu | création, commande, tout le reste |
PUT |
❌ | ✅ | ❌ | représentation complète | remplacement, création à URI connue |
PATCH |
❌ | ❌ par défaut | ❌ | document de patch | modification partielle |
DELETE |
❌ | ✅ | ❌ | inhabituel | suppression |
OPTIONS |
✅ | ✅ | ❌ | inhabituel | découverte, preflight CORS |
TRACE |
✅ | ✅ | ❌ | non | diagnostic, à désactiver en prod |
CONNECT |
❌ | ❌ | ❌ | non | tunnel proxy |
⚡ TL;DR — chaque concept en une ligne
Safe ✓ Le client ne demande aucun changement d'état de la ressource cible. ⚠ N'interdit pas les effets de bord techniques, et ne dit rien de la sécurité (voir TRACE).
Idempotent ✓ Autorise les intermédiaires et les libs de retry à rejouer la requête sans danger. ⚠ Ne promet pas le même code de réponse, et ne règle rien en matière de concurrence.
GET ✓ Le verbe de lecture par défaut : safe, idempotent, cacheable. ⚠ Son body n'a « aucune sémantique définie », et les limites de taille d'URI ne sont pas connaissables à l'avance.
POST ✓ Le seul verbe généraliste : n'importe quelle sémantique définie par la ressource. ⚠ Annonce au réseau « peut modifier, ne rejoue pas, ne cache pas », même quand tu ne fais que lire.
PUT vs PATCH ✓ PUT remplace intégralement et est idempotent ; PATCH applique un document de patch. ⚠ Un champ absent d'un PUT est supprimé, et un PATCH avec opérations relatives n'est pas idempotent.
json-patch vs merge-patch
✓ json-patch est ordonné, cible les tableaux et porte ses pré-conditions (op: test) ; merge-patch est lisible et immédiat.
⚠ merge-patch ne peut ni modifier un élément de tableau, ni stocker null comme valeur : c'est son marqueur de suppression.
DELETE ✓ Idempotent : le second appel laisse le même état serveur. ⚠ Un 404 au second appel est correct, ce n'est pas une violation d'idempotence.
QUERY (RFC 10008)
✓ Safe et idempotent comme GET, avec un body attendu comme POST, et une réponse cacheable.
⚠ Content-Type est obligatoire, le sniffing est interdit, et la cache key doit inclure le body.
Accept-Query ✓ En-tête de réponse qui annonce les media types de requête acceptés. ⚠ C'est un Structured Field, il s'applique à tout le path, et l'ordre n'exprime aucune préférence.
Location vs Content-Location sur QUERY
✓ Location pointe une requête rejouable en GET ; Content-Location pointe le résultat figé reçu.
⚠ Les confondre donne soit un cache périmé présenté comme frais, soit une requête sauvegardée inutilisable.
🎓 À retenir
POST /search n'est pas un détail de style : il interdit activement au CDN de cacher et à la lib HTTP de rejouer une opération pourtant inoffensive.Location. Une requête sauvegardée transforme une lecture complexe en ressource GET, donc en quelque chose de cachable et conditionnable trivialement. Le body n'était que le symptôme.POST /search existants, qui écrasent en général les deux cas en un seul 400.Accept-Query, caching, redirections, exemplesAccept-PatchAccept-Query@RequestMapping, avec liens vers les tickets undici, Jetty et Tomcat