Pourquoi café devient café : déboguer UTF-8, les entités HTML et l'encodage des URL
Vous saisissez café dans un formulaire, l’enregistrez, puis retrouvez café. Ou le paramètre C++ d’une URL devient C . Ou encore une page affiche & au lieu d’une esperluette. Ces problèmes se ressemblent parce qu’ils concernent des caractères particuliers, mais ils proviennent de couches de représentation différentes.
Corriger la mauvaise couche peut aggraver les dégâts. Remplacer partout é par é peut améliorer temporairement une page, sans réparer la chaîne d’encodage ni traiter les autres caractères Unicode.
Ce guide distingue l’encodage des caractères, les entités HTML, l’encodage en pourcentage des URL et la représentation JSON, puis montre comment localiser le défaut avec les outils du navigateur, cURL et JavaScript.
Quatre couches faciles à confondre
Un caractère n’est ni sa suite d’octets transmise ni sa représentation dans du balisage. Prenons ce texte :
café & tea
Plusieurs représentations sont valides selon le contexte :
| Contexte | Exemple | Signification |
|---|---|---|
| Texte Unicode | café & tea | Les caractères que l’application veut manipuler |
| Octets UTF-8 | 63 61 66 C3 A9 20 26 20 74 65 61 | Les octets qui représentent ces caractères |
| Composant d’URL | caf%C3%A9%20%26%20tea | Les octets UTF-8 encodés en pourcentage pour un composant |
| Texte source HTML | café & tea | Du HTML affichant une esperluette littérale |
| Chaîne JSON | "café & tea" | Une chaîne dans un document JSON ; échapper l’accent est facultatif |
UTF-8 est un encodage de caractères. L’encodage en pourcentage représente des octets dans une URL. Les références de caractères HTML appartiennent à la syntaxe HTML, et les échappements JSON à la syntaxe JSON. Ces conversions ne sont pas interchangeables.
L’encodeur et décodeur HTML traite les références telles que & et <. L’encodeur et décodeur d’URL traite l’encodage en pourcentage, les valeurs de formulaire et les paramètres de requête.
Problème 1 : café indique souvent un mauvais décodage
La lettre é est représentée en UTF-8 par les deux octets C3 A9. Interprétés à tort comme des caractères Windows-1252, ils s’affichent sous la forme é. Les octets ne contenaient pas intrinsèquement le mauvais mot : ils ont été lus avec le mauvais encodage.
Ce texte déformé est souvent appelé mojibake. Une connexion à une base de données, un import de fichier, un en-tête de réponse ou un ancien service peut employer un encodage différent de celui du producteur des octets.
Examinez tout le trajet des données :
- Comment le texte a-t-il été saisi ou importé ?
- Quel encodage a servi à l’écrire dans un fichier, une base ou un message ?
- A-t-il été converti à nouveau lors de sa lecture ou de son export ?
- Quels octets le serveur envoie-t-il réellement ?
- Comment le destinataire choisit-il l’encodage de décodage ?
Un document HTML devrait normalement déclarer UTF-8 au début :
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Text example</title>
</head>
<body>
<p>café & tea</p>
</body>
</html>
Pour une page servie par HTTP, vérifiez également l’en-tête de réponse, par exemple :
Content-Type: text/html; charset=utf-8
Un encodage explicite dans l’en-tête du serveur peut prendre le pas sur la déclaration du document. Une chaîne déjà endommagée dans la base ne sera pas réparée par une déclaration UTF-8 : celle-ci décrit les octets transmis, elle ne les corrige pas.
Problème 2 : le texte est déjà corrompu à son arrivée
Ouvrez le panneau Réseau du navigateur, sélectionnez la requête qui fournit le contenu et comparez les en-têtes, la réponse et la page affichée. Le guide d’inspection des requêtes réseau décrit cette démarche dans Chrome, Firefox, Edge et Safari.
Pour un contrôle reproductible de la réponse du serveur, enregistrez les en-têtes et les octets avec cURL :
curl --compressed --silent --show-error \
--dump-header headers.txt \
--output response.bin \
'https://example.com/api/profile'
--compressed autorise cURL à décompresser les formats HTTP qu’il prend en charge. Cette opération est distincte du décodage des caractères. Pour une API JSON, vérifiez que le corps est bien du UTF-8 valide et que le serveur renvoie le type de média attendu.
Enregistrez ce code dans check-response.mjs, puis lancez node check-response.mjs pour examiner les octets avec Node.js :
import { readFileSync } from "node:fs";
const bytes = readFileSync("response.bin");
try {
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
console.log(text.slice(0, 200));
} catch (error) {
console.error("Response contains invalid UTF-8 byte sequences", error);
}
Un décodage UTF-8 réussi prouve que la séquence d’octets est bien formée. Il ne prouve pas que le texte n’a jamais été corrompu : une chaîne contenant déjà café peut être correctement encodée en UTF-8. Comparez donc une valeur connue à chaque frontière de stockage et de transport.
Le générateur de commandes cURL peut préparer une requête reproductible avec des en-têtes personnalisés. Il ne récupère ni ne valide la réponse : examinez celle-ci dans votre terminal.
Problème 3 : un signe plus ou un espace change dans une URL
L’encodage des URL possède ses propres règles. En JavaScript, encodeURIComponent convient à une valeur de paramètre, pas à une URL entière :
const value = "café & tea";
console.log(encodeURIComponent(value));
// caf%C3%A9%20%26%20tea
Construire l’URL à la main est une source d’erreurs :
// Fragile: '&' becomes a query separator.
const bad = "/search?q=" + "café & tea";
// Safer: the URL API encodes and manages the query parameter.
const url = new URL("https://example.com/search");
url.searchParams.set("q", "café & tea");
console.log(url.toString());
// https://example.com/search?q=caf%C3%A9+%26+tea
Pourquoi un signe plus ? URLSearchParams utilise la sérialisation des formulaires, où les espaces deviennent +. encodeURIComponent utilise au contraire %20. Un signe plus littéral dans une valeur de formulaire doit devenir %2B :
const query = new URLSearchParams({ language: "C++" });
console.log(query.toString()); // language=C%2B%2B
Décodez les valeurs selon le format qui les a produites. Pour une URL complète, laissez URL et URLSearchParams gérer sa structure. L’annonce de l’encodeur, décodeur et éditeur de paramètres d’URL présente les paramètres répétés et la distinction entre espaces et signes plus.
Évitez de décoder plusieurs fois une URL signée ou de la reconstruire sans précaution. Modifier l’encodage en pourcentage ou l’ordre des paramètres peut invalider une signature fondée sur la représentation exacte de l’URL.
Problème 4 : la page affiche & au lieu de &
Le HTML Tom & Jerry s’affiche normalement sous la forme Tom & Jerry. Si les cinq caractères & apparaissent littéralement, les données ont peut-être été échappées deux fois, ou insérées avec une API textuelle après avoir été échappées.
Ce code convient lorsque value contient du texte ordinaire saisi par l’utilisateur :
const value = "Tom & Jerry";
document.querySelector("#result").textContent = value;
Ne remplacez pas d’abord l’esperluette par & avant d’affecter la chaîne à textContent. Le navigateur traitera alors cette séquence comme du texte, sans l’interpréter comme du balisage.
À l’inverse, ne passez jamais à innerHTML uniquement pour décoder une entité si un utilisateur peut contrôler le contenu : cela peut introduire une faille XSS. Pour afficher du texte littéral, utilisez textContent. Pour du HTML enrichi autorisé, utilisez un outil d’assainissement maintenu et adapté au contexte. Le guide sur l’échappement et l’assainissement HTML explique cette distinction.
L’outil HTML permet d’essayer des exemples. En production, la correction consiste généralement à échapper une seule fois, au bon endroit, avec le moteur de rendu ou une bibliothèque adaptée au contexte de sortie.
Problème 5 : JSON, bases de données et formulaires divergent
JSON est un format de données, pas une alternative à UTF-8. Une réponse JSON peut contenir é directement ou l’échappement Unicode \u00e9 : après une analyse correcte, ils représentent le même caractère.
Pour la plupart des API modernes, envoyez du JSON encodé en UTF-8 avec Content-Type: application/json et validez le corps comme du JSON. N’appliquez pas un encodage d’URL à une chaîne JSON, sauf si l’API exige explicitement du JSON dans une URL ou un champ de formulaire.
Les formulaires peuvent utiliser application/x-www-form-urlencoded ou multipart/form-data, dont les règles diffèrent. Envoyer des valeurs de formulaire à un serveur qui attend du JSON crée un désaccord de format, pas nécessairement un problème d’accent. Le guide JSON, formulaires et multipart compare ces cas.
Vérifiez aussi les données des colonnes et l’encodage de la connexion à la base, sans vous fier uniquement au meta charset de la page. Si un import a définitivement remplacé des caractères par �, une sauvegarde ou une copie intacte peut être nécessaire pour les récupérer.
Tableau de diagnostic rapide
| Symptôme | Premier point à examiner | Correction habituelle |
|---|---|---|
café au lieu de café | Octets d’origine, stockage, encodage de connexion | Assurer UTF-8 à chaque frontière et réparer la source si possible |
� à la place d’un caractère | Erreur de décodage ou import avec perte | Récupérer les octets d’origine et supprimer la conversion incorrecte |
& littéral à l’écran | Échappement HTML et insertion dans le DOM | Éviter le double échappement ; utiliser textContent pour le texte brut |
+ interprété comme un espace | Décodeur de paramètres ou de formulaire | Encoder le + littéral en %2B dans les valeurs de formulaire |
%25C3%25A9 dans un paramètre | Encodage en pourcentage répété | Encoder une fois, à la bonne frontière |
| JSON correct mais page HTML incorrecte | Types de contenu et encodages différents | Comparer les en-têtes, les modèles et l’échappement de sortie |
Une méthode fiable pour le texte international
Créez un petit jeu de régression avec des accents, du texte non latin, un emoji, un signe plus et une esperluette. Vérifiez-le à chaque étape : formulaire, serveur, base, réponse API, DOM et fichier exporté. Conservez les résultats attendus sous forme de chaînes Unicode et gardez les octets bruts pendant le diagnostic.
Si votre application envoie aussi du texte multilingue à un modèle d’IA, l’encodage et la tokenisation sont deux sujets distincts. Le compteur de tokens IA illustre le découpage en tokens, mais ne valide ni ne répare le transport UTF-8.
Pour aller plus loin
- Standard Encoding du WHATWG (original normatif en anglais)
- Standard URL du WHATWG (original normatif en anglais)
- MDN : TextDecoder (en anglais)
Pour un premier diagnostic, examinez une valeur défectueuse séparément dans l’encodeur et décodeur HTML et l’encodeur et décodeur d’URL. Identifier sa représentation permet souvent de trouver rapidement la bonne correction.