09 oct. 2025·8 min de lecture

Incohérences de contrat frontend-backend qui font échouer les sauvegardes de formulaires

Les divergences de contrat frontend-backend font que les formulaires semblent réussir alors que rien n'est sauvegardé. Alignez DTOs, erreurs de validation, codes d'état et formats de pagination.

Incohérences de contrat frontend-backend qui font échouer les sauvegardes de formulaires

Ce que signifie « le formulaire s'envoie mais rien n'est enregistré »

Un contrat frontend-backend est l'accord partagé entre votre formulaire et votre API : quels champs sont envoyés, comment ils s'appellent, leurs types, et ce que l'API renvoie en cas de succès ou d'erreur.

Quand un formulaire « s'"envoie" » mais que rien n'est enregistré, cela veut généralement dire que le frontend a fait son travail (il a envoyé une requête), mais que le backend n'a pas persisté les données. Le problème délicat, c'est que l'utilisateur voit souvent aucune erreur claire parce que la réponse semble OK, l'interface ignore la réponse, ou l'API renvoie une forme inattendue.

Un exemple courant : le formulaire poste { email, password }, mais l'API attend { userEmail, passwordHash }. La requête arrive au serveur, la validation échoue, et l'API renvoie un 200 générique avec { ok: false } (ou un corps vide). Le frontend considère tout 200 comme un succès, affiche une notification, et l'utilisateur passe à autre chose alors que la base de données n'a jamais changé.

Cela arrive souvent dans des prototypes construits rapidement ou générés par IA. Les outils peuvent générer vite une UI et une API, mais devinent souvent les noms de champs, oublient les règles de validation côté serveur ou inventent des formats de réponse. Quand on remplace ensuite un endpoint simulé par un réel, le « contrat » bouge légèrement et les sauvegardes commencent à échouer silencieusement.

Corriger ça ne se limite pas à un seul fichier. Il faut généralement aligner plusieurs éléments dans toute la stack :

  • Request DTOs (noms de champs, types, requis vs optionnels)
  • Erreurs de validation (une forme cohérente que l'UI peut afficher à côté des champs)
  • Codes d'état (pour que succès et échec soient sans ambiguïté)
  • Réponses de succès (retourner ce dont l'UI a besoin pour confirmer la sauvegarde)
  • Réponses de listes (format de pagination qui ne change pas selon l'endpoint)

Si vous avez hérité d'une appli générée par IA et que les sauvegardes sont instables, c'est précisément le genre de problème que FixMyMess rencontre : le code « fonctionne », mais le contrat est incohérent. Le reste de ce guide explique comment rendre ce contrat explicite, prévisible et difficile à casser par accident.

Symptômes courants et ce qu'ils indiquent en général

Un formulaire peut sembler avoir fonctionné alors que rien n'a changé. L'indice le plus fréquent est une notification de succès (ou une coche verte), mais après actualisation les anciennes données sont toujours là. Cela signifie souvent que l'UI a considéré le signal erroné comme un succès, pas ce que le serveur a réellement fait.

Côté backend, méfiez-vous des réponses qui semblent « réussies » mais ne le sont pas. Un problème de contrat classique est un 200 OK contenant une erreur dans le corps JSON (par exemple, { "ok": false, "message": "invalid" }). Un autre cas est 204 No Content alors que le frontend attend l'enregistrement sauvegardé et a besoin d'un id ou de champs mis à jour.

Côté dev, les indices sont souvent petits et faciles à ignorer : un log console affiche undefined pour un champ que vous êtes sûr d'avoir rempli, ou l'onglet Réseau montre une forme de réponse que vous n'avez pas prévue (comme data vs result, ou un tableau alors que vous attendiez un objet). Ce sont des mismatches de contrat frontend-backend en pleine vue.

Symptômes courants et leurs causes probables :

  • Message de succès affiché, mais après rafraîchissement rien n'a changé : la requête n'a jamais atteint l'endpoint de sauvegarde, ou elle l'a fait avec des champs manquants/renommés.
  • La sauvegarde fonctionne seulement pour certains utilisateurs : la validation backend diffère des règles frontend, ou des champs requis dépendent du rôle utilisateur.
  • Le backend renvoie 200, mais l'UI se comporte bizarrement : l'erreur est encodée dans le JSON, pas via le code d'état.
  • L'UI affiche « Sauvé » mais la liste montre toujours l'ancienne entrée : cache non invalide, ou la réponse ne contient pas l'enregistrement mis à jour.
  • La pagination semble cassée (éléments manquants, répétitions) : le frontend attend page/total, le backend renvoie nextCursor/items (ou l'inverse).

Règle rapide : faites confiance à l'onglet Réseau plutôt qu'à l'UI. Si le payload de la requête et la réponse ne correspondent pas à ce que votre code suppose, le formulaire peut « s'envoyer » sans jamais sauvegarder.

C'est un schéma fréquent que nous voyons chez FixMyMess quand un prototype généré par IA branche le bouton et la notification, mais ne confirme jamais que le serveur a persisté quoi que ce soit.

Le contrat sur lequel il faut s'entendre : formes, noms et types

Quand une sauvegarde « fonctionne » dans l'UI mais que rien ne change en base, ce n'est souvent pas un bug isolé. C'est un désaccord entre le client et l'API sur ce qu'une requête et une réponse valides doivent contenir. Beaucoup de mismatches frontend-backend viennent de détails ennuyeux, mais ils cassent l'appli silencieusement.

Commencez par écrire le contrat minimal pour une seule sauvegarde. Si une partie est vague, différentes parties de la stack rempliront les blancs différemment.

  • Endpoint + méthode (par exemple, POST /users vs PUT /users/:id)
  • Headers requis (surtout Content-Type et l'auth)
  • Forme du corps de la requête (noms de champs, structure, optionnel vs requis)
  • Forme de la réponse (ce que le client doit lire pour mettre à jour l'UI)
  • Forme des erreurs (comment les problèmes de validation sont retournés)

Le nommage est le premier endroit où le contrat dérive. Si le frontend envoie firstName mais que le backend attend first_name, vous pouvez obtenir une réponse « réussie » alors que le backend ignore le champ inconnu ou stocke une valeur par défaut.

Les types sont le second point. Cas fréquent : l'UI envoie age: "32" comme chaîne, mais le backend attend un nombre. Certains frameworks coercissent, d'autres rejettent, certains convertissent l'échec en null. Si null est autorisé, vous vous retrouvez à sauvegarder une valeur vide sans le remarquer.

Les champs en trop ou manquants peuvent aussi disparaître silencieusement. Par exemple, le formulaire inclut marketingOptIn, mais le DTO côté serveur ne l'inclut pas. Selon la stack, ce champ peut être ignoré lors de la désérialisation sans erreur. L'inverse est douloureux : le backend exige companyId, mais le frontend ne l'envoie jamais, donc le serveur crée un enregistrement non rattaché.

Une façon pratique de détecter tôt ces problèmes est de prendre une requête réelle depuis les outils dev du navigateur, la comparer au DTO et aux règles de validation du serveur ligne par ligne, et s'entendre sur les noms et types exacts avant de toucher la logique.

Étape par étape : aligner les DTOs des champs du formulaire jusqu'à la base

Quand un formulaire « s'envoie » mais que rien n'est enregistré, la cause habituelle est simple : le frontend envoie une forme, le backend en attend une autre. La correction commence par décider qui définit la vérité, puis vérifier chaque étape du formulaire jusqu'au stockage.

1) Choisir une source unique de vérité

Choisissez un endroit qui définit noms et types. Pour la plupart des équipes, les DTOs de requête/réponse du backend sont la source la plus sûre, car ils sont proches de la validation et de la persistance. Si vous utilisez un schéma partagé, traitez-le comme un contrat et versionnez-le.

2) Écrire les DTOs avec des exemples réels

Ne comptez pas sur le « c'est évident ». Écrivez un exemple pour la création et un pour la mise à jour. Les mises à jour échouent souvent parce qu'elles exigent un id, autorisent des champs partiels ou utilisent des noms différents.

// Create
{ "email": "[email protected]", "displayName": "Sam", "marketingOptIn": true }

// Update
{ "id": "usr_123", "displayName": "Sam Lee", "marketingOptIn": false }

Ensuite écrivez le DTO de réponse dont l'UI a réellement besoin. Si l'UI attend user.id mais que l'API renvoie userId, les sauvegardes peuvent « fonctionner » sans que l'UI ne puisse afficher l'état mis à jour.

3) Tracer le chemin du formulaire jusqu'à la base

Parcourez la chaîne complète une fois, bout à bout :

  • Noms et types des champs du formulaire (chaînes, nombres, booléens)
  • Payload envoyé sur le réseau (y compris les headers comme le content type)
  • Parsing et validation du DTO côté backend (champs requis, valeurs par défaut)
  • Mapping vers les colonnes de la base (noms et conversions de type)
  • Corps de la réponse que l'UI lit pour mettre à jour l'écran

4) Vérifier avec le payload exact que l'UI envoie

Copiez la requête réelle depuis l'onglet réseau du navigateur et rejouez-la. Cela attrape des problèmes comme "true" (string) vs true (boolean), champs manquants ou nesting inattendu.

5) Changer un côté, puis retester un exemple connu bon

Corrigez soit le mapping frontend, soit le DTO backend, pas les deux en même temps. Gardez un payload « golden » et une réponse attendue pour confirmer que vous n'avez pas juste déplacé le problème.

Si vous avez hérité d'un code généré par IA, ces écarts sont courants parce que les UIs et APIs générées évoluent séparément. Des plateformes comme FixMyMess commencent souvent par auditer le contrat et les points de mapping avant de toucher la logique métier, car c'est là que se cachent les échecs silencieux de sauvegarde.

Rendre les erreurs de validation cohérentes et faciles à afficher

Confirm Saves End to End
We trace the exact payload from your form through the API to the database.

Quand le frontend et le backend ne s'accordent pas sur la forme des erreurs, les utilisateurs vivent la pire expérience : le formulaire « s'envoie », mais rien ne leur dit quoi corriger. Une forme d'erreur simple et prévisible est l'une des victoires les plus faciles contre les mismatches de contrat.

Un pattern pratique est de toujours retourner la même structure pour les échecs de validation :

{
  "error": {
    "type": "validation_error",
    "fields": [
      { "field": "email", "code": "invalid_format", "message": "Enter a valid email." },
      { "field": "password", "code": "too_short", "message": "Password must be at least 12 characters." }
    ],
    "non_field": [
      { "code": "state_conflict", "message": "This invite has already been used." }
    ]
  }
}

Gardez trois éléments pour chaque problème : le nom du champ (correspondant à votre DTO), un code stable (pour que l'UI réagisse), et un message humain (pour l'affichage). Si vous ne retournez que des messages, l'UI finit par deviner et casse lorsque les textes changent.

Les erreurs globales sont tout aussi importantes. Les permissions, conflits d'état et limites de taux ne sont pas liées à un champ, elles doivent donc aller dans un endroit séparé comme non_field (ou global). L'UI peut les afficher près du bouton d'envoi ou sous forme de petite bannière.

Côté frontend, le mapping doit être ennuyeux et régulier :

  • Effacer les erreurs précédentes avant l'envoi.
  • Pour chaque item de fields[], attacher message à l'input correspondant.
  • Si le champ est inconnu, le traiter comme une erreur globale (souvent signe d'un drift de DTO).
  • Afficher les messages non_field[] dans un emplacement visible.

Enfin, ne cachez pas la validation dans des réponses « réussies ». Si la sauvegarde a échoué, retournez une réponse d'erreur avec un corps d'erreur. Mélanger des avertissements dans un 200 est la façon dont vous obtenez des échecs silencieux, surtout dans les applis générées par IA que nous voyons chez FixMyMess.

Codes d'état et réponses de succès qui ne mentent pas

Beaucoup de mismatches commencent par un mensonge simple : le serveur renvoie un statut de succès, mais l'UI ne peut pas savoir si la sauvegarde a réellement eu lieu. Si le frontend considère tout 200 comme « sauvegardé », vous obtenez le classique « la notification dit succès, mais après rafraîchissement rien ».

Utilisez les codes d'état comme signal clair, et gardez la forme de la réponse honnête.

Un pattern simple et prévisible

Choisissez des règles que vous pouvez suivre à chaque fois :

  • 201 Created quand une nouvelle ressource est créée, et incluez la ressource dans le corps.
  • 200 OK pour les lectures et mises à jour, avec un corps JSON représentant l'état sauvegardé.
  • 204 No Content uniquement quand vous ne retournez vraiment aucun corps (et que le client n'a pas besoin de nouvelles données).
  • 422 Unprocessable Entity pour les problèmes de validation (erreurs de champs que l'utilisateur peut corriger).
  • 409 Conflict pour les doublons ou les conflits de version (la requête est valide mais ne peut pas être appliquée telle quelle).

Retourner 200 OK avec un { error: ... } est un piège. Beaucoup de frontends ne regardent que response.ok ou le code HTTP. L'UI affichera un succès pendant que le backend a refusé la sauvegarde.

Idempotence, doublons et comportement « réessayer »

Si les utilisateurs peuvent double-cliquer sur Enregistrer, rafraîchir pendant la sauvegarde, ou réessayer après un timeout, vous avez besoin d'une règle claire pour les doublons.

Utilisez 409 quand la même valeur unique existe déjà (par exemple, email unique) ou quand le locking optimiste échoue (stale updatedAt ou version). Utilisez 422 quand le payload lui-même est invalide (champs requis manquants, format invalide).

Ce qu'une sauvegarde réussie devrait renvoyer

Même pour les mises à jour, retournez les données canoniques que le serveur a stockées, pas un écho de ce que le client a envoyé. Une bonne réponse de sauvegarde inclut généralement :

  • id
  • updatedAt (ou version)
  • les champs normalisés (chaînes trimées, valeurs par défaut calculées)
  • toutes les valeurs générées côté serveur (slugs, statuts)

Exemple : si le frontend envoie " Acme ", la réponse devrait renvoyer "Acme". Ainsi l'UI correspond immédiatement à la réalité et vous détectez tôt les problèmes de contrat. Les équipes apportent souvent des APIs générées par IA cassées à FixMyMess où une réponse « réussie » cachait en réalité une sauvegarde refusée derrière un 200.

Formats de pagination qui restent stables dans toute la stack

La pagination est un contrat, pas un détail d'implémentation. Si le frontend et le backend ne sont pas d'accord sur la forme, vous obtenez des tableaux vides, des lignes répétées ou un « Charger plus » qui ne finit jamais. Ces mismatches sont fréquents dans les APIs générées par IA où l'UI et le serveur ont été scaffoldés séparément.

Choisissez un style de pagination et nommez-le clairement

La façon la plus rapide d'éviter la confusion est de choisir un style et d'écrire les paramètres exacts :

  • page + pageSize : simple pour les numéros de page, mais peut être lent si la base doit compter et sauter beaucoup.
  • offset + limit : facile à implémenter, mais insertions/suppressions peuvent causer doublons ou éléments manquants.
  • cursor : idéal pour le « infinite scroll », stable lors des changements, mais nécessite un token cursor et un ordre de tri strict.

Une fois choisi, gardez-le cohérent sur les endpoints. Une UI construite pour page=3&pageSize=20 ne fonctionnera pas correctement si un endpoint attend offset=40&limit=20.

Verrouillez la forme de la réponse que l'UI lit

Décidez des champs exacts sur lesquels le frontend peut compter. Un défaut sûr : items plus un indicateur pour savoir s'il y a plus de données. Les totaux sont optionnels et peuvent être coûteux.

Un mismatch très courant est le backend renvoyant { data: [...] } alors que l'UI attend [...] (ou items). La requête réussit, l'UI n'affiche rien, et personne ne voit d'erreur.

Pour éviter le reshuffling des pages, figez ces règles dans le contrat :

  • Toujours exiger un tri déterministe (par exemple sort=createdAt:desc).
  • Appliquer les filtres avant la pagination, et retourner les filtres appliqués si possible.
  • Pour la pagination par curseur, basez le cursor sur les mêmes champs de tri que vous retournez.
  • Soyez cohérent sur les états vides : renvoyez items: [] avec hasMore: false.

Quand FixMyMess audite des prototypes cassés, la pagination instable est souvent la cause masquée des rapports « rien ne s'enregistre » parce que l'enregistrement existe bien, mais n'apparaît jamais dans la vue liste que l'utilisateur vérifie juste après la sauvegarde.

Pièges courants qui causent des échecs silencieux de sauvegarde

Prepare for Production Deployment
Prepare your app for deployment with repairs, hardening, and verification.

Un formulaire peut sembler sain et pourtant ne pas persister car l'UI et l'API divergent sur de petits détails faciles à manquer. Ces mismatches n'entraînent souvent pas d'erreur évidente, donc on suppose que la base est défaillante alors que le problème vient de la forme de la requête.

Un piège courant est la coercition JSON silencieuse. Le frontend envoie une chaîne là où l'API attend un nombre, ou envoie une chaîne vide pour un champ nullable. Certains serveurs éliminent discrètement les champs qu'ils ne peuvent pas parser, puis la sauvegarde échoue plus loin parce que le champ manquant est requis.

Autre classique : des champs qui paraissent optionnels dans l'UI mais sont requis pour sauvegarder. Les applis multi-tenant demandent souvent tenantId, orgId ou userId. Si ceux-ci proviennent normalement du contexte d'auth, un petit bug d'auth peut les rendre vides sans changer le formulaire.

Les dates causent aussi des échecs subtils. Un datepicker peut envoyer "01/02/2026" alors que l'API attend ISO comme "2026-02-01". Les fuseaux horaires peuvent aussi décaler les valeurs. Vous sauvegardez "14 janv" mais le serveur stocke "13 janv" en UTC, et on pense que la sauvegarde a raté.

Les mismatches du contexte d'auth sont sournois. L'UI vous montre connecté parce qu'elle a un token, mais l'API traite la requête comme anonyme car l'en-tête est manquant, le cookie bloqué ou le token expiré.

Les updates optimistes peuvent tout cacher. L'écran se met à jour comme si la sauvegarde avait réussi, mais le backend a rejeté la requête.

Surveillez ces signaux :

  • L'onglet réseau montre 200, mais le corps de la réponse dit "error" ou renvoie un enregistrement inchangé.
  • L'API renvoie 204 et l'UI ne peut pas confirmer ce qui a réellement été sauvegardé.
  • Des IDs requis manquent dans le payload, mais aucune erreur de champ n'est affichée.
  • Une date semble correcte dans l'UI mais diffère en base.
  • L'UI se met à jour avant que l'appel API ne soit terminé.

Quand nous auditons des applis générées par IA chez FixMyMess, nous trouvons souvent deux formes de DTO différentes utilisées sur des écrans distincts, donc une page sauvegarde et une autre échoue silencieusement alors qu'elles utilisent les mêmes champs de formulaire.

Checklist rapide avant mise en production

Un formulaire qui « s'envoie » n'est pas la même chose que des données effectivement sauvegardées. Avant de livrer, faites un contrôle rapide du contrat couvrant tout le chemin : champs UI, DTOs API, validation et ce que le serveur renvoie.

Commencez par collecter des exemples réels, pas juste des types. Mettez une requête d'exemple et une réponse d'exemple à côté du code et confirmez qu'elles correspondent à ce que le serveur reçoit et renvoie. Faites attention au nommage (camelCase vs snake_case), aux champs optionnels vs requis, et aux subtilités de types comme des nombres qui arrivent sous forme de chaînes.

Voici une courte checklist qui attrape la plupart des échecs silencieux :

  • Confirmer que les DTOs de create et update correspondent exactement aux champs UI (noms, types, et quels champs peuvent être null ou manquants).
  • Faire en sorte que chaque sauvegarde échouée retourne un statut non 2xx, plus un corps d'erreur cohérent (message général et erreurs par champ).
  • S'assurer que l'UI peut mapper les erreurs serveur aux noms d'input visibles par l'utilisateur (pas de emailAddress côté serveur si le formulaire utilise email).
  • Vérifier que tous les endpoints de liste utilisent le même format de pagination de réponse (clé items, total, page/limit, et où vivent les métadonnées).
  • Tester une vraie création et une vraie mise à jour bout à bout avec un enregistrement réel en base, puis rafraîchir la page et confirmer que les valeurs persistent.

Un test pratique : soumettez intentionnellement un champ invalide (comme un mot de passe trop court). Si l'UI affiche une notification de succès, ou si le réseau montre 200 alors que rien n'a changé, votre contrat ment quelque part.

Si vous avez hérité d'une appli générée par IA, c'est là que les problèmes s'accumulent : les DTOs dérivent, les formats d'erreur varient par endpoint, et la pagination est réinventée par écran. Des équipes comme FixMyMess commencent souvent par un audit ciblé sur ces contrats pour que les sauvegardes deviennent prévisibles avant d'ajouter d'autres fonctionnalités.

Un exemple réaliste : la sauvegarde paraît OK, mais les données sont incorrectes

Make Your API Contract Explicit
Fix DTOs, status codes, and responses so your UI can trust every save.

Une histoire fréquente : un formulaire d'inscription affiche une notification de succès, mais l'utilisateur ne peut pas se connecter ensuite. Tout le monde suppose « l'auth est cassée », mais le vrai bug vient de la forme de la requête et de la réponse.

Le frontend envoie ce payload :

{
  "email": "[email protected]",
  "password": "P@ssw0rd!",
  "passwordConfirm": "P@ssw0rd!"
}

L'API attend password_confirmation (snake_case) et ignore passwordConfirm. Si l'API renvoie aussi 200 OK avec un générique { "success": true }, l'UI va célébrer alors que le serveur n'a jamais validé la confirmation et peut même stocker une mauvaise valeur ou rejeter en interne.

La correction est ennuyeuse mais efficace : s'entendre sur un DTO et un format d'erreur. Soit renommez le champ dans l'UI, soit acceptez les deux clés côté serveur et mappez-les vers le même DTO.

En cas de succès, retournez quelque chose qui prouve que la sauvegarde a eu lieu :

{
  "id": "usr_123",
  "email": "[email protected]"
}

Utilisez 201 Created pour un nouvel utilisateur. En cas d'échec de validation, utilisez 422 Unprocessable Entity et retournez des erreurs par champ que l'UI peut afficher à côté des inputs :

{
  "errors": {
    "password_confirmation": ["Does not match password"]
  }
}

Un second cas se produit sur les pages de liste. Le frontend construit des contrôles de pagination à partir de total, mais l'API ne retourne qu'un cursor et des items. L'UI affiche « Page 1 of 0 » ou désactive Suivant alors qu'il y a plus de données.

Choisissez un style de pagination et tenez-vous-y. Si vous voulez des totaux, retournez items et total. Si vous voulez de la pagination par curseur, retournez items, nextCursor et hasNext, et faites en sorte que l'UI cesse de demander total.

Étapes suivantes : verrouiller le contrat et empêcher les régressions

Les mismatches frontend-backend se répètent souvent pour une raison : le contrat vit dans la tête des gens, pas dans quelque chose que l'on peut vérifier. La solution est ennuyeuse mais efficace : écrivez-le, testez-le, et traitez les changements comme de vraies modifications.

Commencez par une note d'une page pour les endpoints qui comptent le plus (souvent : create, update, list). Gardez-la en langage simple et incluez des exemples concrets.

  • Request DTO : noms de champs, requis vs optionnel, types, et comment envoyer les valeurs vides
  • Response DTO : ce que « succès » renvoie (le record sauvegardé vs seulement un id)
  • Format d'erreur : une forme unique pour validation et erreurs serveur, plus quelques exemples
  • Codes d'état : ce que vous utilisez pour create/update/not found/validation failures
  • Pagination : paramètres et forme de réponse (items, total, page, pageSize)

Ajoutez ensuite un petit ensemble de contrôles de contrat pour les endpoints clés afin de détecter les ruptures le jour même où elles sont introduites. Ça peut être de simples tests de snapshot côté backend, ou un petit script en CI qui poste des payloads connus et vérifie la forme de la réponse et le code d'état.

Choisissez une courte liste de règles « à ne jamais changer silencieusement » et appliquez-les :

  • Les erreurs de validation doivent toujours mapper aux champs (et inclure un message lisible)
  • Le succès ne retourne jamais 200 en cachant un message d'erreur dans le corps
  • La pagination retourne toujours les mêmes clés, même quand items est vide
  • Les DTOs ne renommant jamais un champ sans incrémenter une version ou faire une release coordonnée

Avant d'embellir l'UI, standardisez le format des erreurs API. Une fois que le frontend peut afficher de manière fiable les erreurs par champ, la plupart des rapports « ça s'est sauvegardé mais non » deviennent beaucoup plus clairs.

Si votre codebase a été générée par des outils IA et que les patterns sont incohérents, une passe d'audit et de réparation ciblée est souvent le moyen le plus rapide d'obtenir de la stabilité. FixMyMess propose des audits gratuits puis répare les contrats bout à bout (DTOs, validation, codes d'état, pagination) pour que l'appli se comporte en production comme dans les démos.