Encodage et décodage d’URL : le percent-encoding sans mystère
Vous consultez un journal serveur et tombez sur %E4%BD%A0%E5%A5%BD dans une chaîne de requête. Données corrompues ? Bug ? Rien de tout ça — ce sont les caractères chinois 你好, convertis en trois octets UTF-8 chacun, puis percent-encodés. Tôt ou tard, chaque dev web se retrouve face à ce genre de surprise : ça a l’air cassé, mais l’URL fait exactement ce qu’elle doit faire.
Le percent encoding transforme les caractères incompatibles en séquences %XX que les URL acceptent. On va voir le fonctionnement octet par octet, le choix entre encodeURI et encodeURIComponent, l’encodage correct en quatre langages, et les bugs classiques qui piègent même les devs chevronnés.
Collez n’importe quelle URL dans notre Décodeur et encodeur d’URL pour voir l’encodage et le décodage en temps réel pendant votre lecture.
Qu’est-ce que l’encodage d’URL (percent encoding) ?
Les URL n’acceptent qu’un petit sous-ensemble de caractères ASCII. Lettres, chiffres et quelques symboles passent sans problème. Tout le reste — espaces, esperluettes, texte chinois, emoji — doit être transformé pour voyager dans une URL.
Le percent encoding remplace chaque octet non sûr par % suivi de deux chiffres hexadécimaux. Un espace devient %20, une esperluette %26. Le nom vient de ce préfixe %.
Le RFC 3986 fixe les règles depuis 2005. Il a remplacé le RFC 2396 et clarifié quels caractères sont sûrs, lesquels sont réservés, et comment traiter le texte non-ASCII.
Exemples rapides :
| Entrée | Encodé | Pourquoi |
|---|---|---|
hello world | hello%20world | L’espace n’est pas autorisé dans les URL |
price=10&tax=2 | price%3D10%26tax%3D2 | = et & ont une signification structurelle |
中 | %E4%B8%AD | Non-ASCII → octets UTF-8 → percent-encodé |
🚀 | %F0%9F%9A%80 | Emoji → 4 octets UTF-8 → percent-encodé |
Quels caractères doivent être encodés ?
Le RFC 3986 classe les caractères en trois groupes. Connaître ces groupes vous épargnera des heures de débogage.
Caractères non réservés (jamais encodés)
66 caractères passent tels quels, partout dans une URL :
A-Z a-z 0-9 - . _ ~
Lettres, chiffres, tiret, point, underscore, tilde. Point final.
Caractères réservés (selon le contexte)
Ces caractères délimitent la structure des URL :
| Caractère | Rôle dans la structure de l’URL |
|---|---|
: | Sépare le schéma de l’autorité (https:) |
/ | Sépare les segments du chemin |
? | Démarre la chaîne de requête |
# | Démarre le fragment |
& | Sépare les paramètres de requête |
= | Sépare la clé du paramètre de sa valeur |
@ | Sépare les informations utilisateur de l’hôte |
+ ! $ ' ( ) * , ; [ ] | Divers rôles réservés |
Principe simple : un caractère réservé qui joue son rôle structurel reste tel quel. S’il apparaît comme donnée — dans une valeur de paramètre, par exemple — on l’encode.
Tout le reste (toujours encodé)
Espaces, chevrons, accolades, barres verticales, antislashs, caractères non-ASCII (chinois, arabe, emoji) — tout ça passe au percent-encoding avant d’entrer dans une URL.
Cas particulier de l’espace : le RFC 3986 le transforme en %20, mais les formulaires HTML utilisent +. On détaille ce conflit plus bas.
Comment fonctionne l’encodage d’URL : le pipeline UTF-8
Pour l’ASCII, c’est simple : valeur hexadécimale de l’octet, % devant. Un espace (octet 32, hex 20) donne %20.
Le texte non-ASCII passe par trois étapes :
Étape 1 — Caractère vers code point Unicode.
é = U+00E9. 🚀 = U+1F680.
Étape 2 — Code point vers octets UTF-8.
UTF-8 utilise 1 à 4 octets selon la plage. é (U+00E9) donne deux octets : 0xC3 0xA9. La fusée (U+1F680), quatre : 0xF0 0x9F 0x9A 0x80.
Étape 3 — Chaque octet devient %XX.
Un triplet percent-encodé par octet.
Le pipeline complet, pour différents types de caractères :
| Caractère | Code Point | Octets UTF-8 | Encodé | Facteur d’expansion |
|---|---|---|---|---|
A | U+0041 | 41 | A (non encodé) | 1× |
| espace | U+0020 | 20 | %20 | 3× |
é | U+00E9 | C3 A9 | %C3%A9 | 6× |
中 | U+4E2D | E4 B8 AD | %E4%B8%AD | 9× |
🚀 | U+1F680 | F0 9F 9A 80 | %F0%9F%9A%80 | 12× |
Vérifiez par vous-même en JavaScript :
const char = '中';
const encoded = encodeURIComponent(char);
console.log(encoded); // '%E4%B8%AD'
// Trace the bytes
const bytes = new TextEncoder().encode(char);
console.log([...bytes].map(b => '%' + b.toString(16).toUpperCase()).join(''));
// '%E4%B8%AD' — matches
Cette expansion compte quand on approche des limites de longueur d’URL. 20 caractères chinois ajoutent 180 caractères de percent-encoding.
encodeURI vs encodeURIComponent — Laquelle choisir ?
Confondre ces deux fonctions JavaScript est l’erreur d’encodage la plus fréquente. Elles se ressemblent, mais encodent des jeux de caractères très différents.
encodeURI() | encodeURIComponent() | |
|---|---|---|
| Objectif | Encoder une URL complète | Encoder un seul composant (clé ou valeur de paramètre) |
| Préserve | : / ? # & = @ + $ , | Aucun de ces caractères |
| Encode | Espaces, non-ASCII, certains signes de ponctuation | Tout sauf A-Z a-z 0-9 - _ . ~ ! ' ( ) * |
| Quand l’utiliser | Vous avez une URL complète avec des espaces ou de l’Unicode dans le chemin | Vous construisez des paramètres de requête à partir d’une saisie utilisateur |
Un bug qui passe en production plus souvent qu’on ne l’imagine :
// ❌ BUG: encodeURI does NOT encode &
const search = 'Tom & Jerry';
const bad = `https://api.example.com/search?q=${encodeURI(search)}`;
// Result: https://api.example.com/search?q=Tom%20&%20Jerry
// The & splits the query string — server sees q=Tom%20 and a separate param %20Jerry
// ✅ FIX: encodeURIComponent encodes & as %26
const good = `https://api.example.com/search?q=${encodeURIComponent(search)}`;
// Result: https://api.example.com/search?q=Tom%20%26%20Jerry
En résumé : dans le doute, prenez encodeURIComponent(). C’est le bon choix dans 95 % des cas.
Testez les deux modes côte à côte dans notre outil d’encodage d’URL →
Encodage d’URL dans chaque langage
JavaScript (navigateur et Node.js)
// Encode a parameter value
const value = encodeURIComponent('price >= 100 & currency = €');
// 'price%20%3E%3D%20100%20%26%20currency%20%3D%20%E2%82%AC'
// Decode
const original = decodeURIComponent(value);
// 'price >= 100 & currency = €'
// Modern approach: URLSearchParams handles encoding automatically
const params = new URLSearchParams({ q: 'hello world', lang: '中文' });
console.log(params.toString());
// 'q=hello+world&lang=%E4%B8%AD%E6%96%87'
// Note: URLSearchParams uses + for spaces (form encoding)
Python
from urllib.parse import quote, unquote, urlencode
# Encode a path segment
quote('hello world/file name.txt', safe='/')
# 'hello%20world/file%20name.txt'
# Encode query parameters
urlencode({'q': '你好', 'page': '1'})
# 'q=%E4%BD%A0%E5%A5%BD&page=1'
# quote_plus uses + for spaces (form encoding)
from urllib.parse import quote_plus
quote_plus('hello world') # 'hello+world'
quote('hello world') # 'hello%20world'
Go
import "net/url"
// Encode a query value (uses + for spaces)
url.QueryEscape("hello world & more")
// "hello+world+%26+more"
// Encode a path segment (uses %20 for spaces)
url.PathEscape("hello world & more")
// "hello%20world%20&%20more"
// Build a URL safely with url.Values
params := url.Values{}
params.Set("q", "你好世界")
params.Set("page", "1")
fmt.Println(params.Encode())
// "page=1&q=%E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C"
Java
import java.net.URLEncoder;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
// Encode (uses + for spaces — Java follows form encoding)
String encoded = URLEncoder.encode("hello world & more", StandardCharsets.UTF_8);
// "hello+world+%26+more"
// For RFC 3986 compliance, replace + with %20
String rfc3986 = encoded.replace("+", "%20");
// "hello%20world%20%26%20more"
// Decode
String decoded = URLDecoder.decode(encoded, StandardCharsets.UTF_8);
// "hello world & more"
Go et Java encodent les espaces en + par défaut (encodage de formulaire). Pour du RFC 3986, remplacez + par %20 en post-traitement.
Cinq bugs d’encodage d’URL qui cassent la production
1. Double encodage (%2520 au lieu de %20)
Vous encodez une chaîne. Elle traverse un framework qui l’encode une seconde fois. Le % de %20 devient %25, et le serveur reçoit le texte littéral %20 au lieu d’un espace.
Symptôme : les URL contiennent %2520, %253D ou d’autres motifs %25xx.
Diagnostic : %25 dans une URL est presque toujours un double encodage — le % lui-même a été encodé.
Correction : décodez d’abord, puis encodez une seule fois. Ne lancez jamais un encodage sans vérifier si la chaîne l’est déjà.
// Detect double encoding
function isDoubleEncoded(str) {
return /%25[0-9A-Fa-f]{2}/.test(str);
}
// Safe encode: decode first, then encode
function safeEncode(str) {
try { str = decodeURIComponent(str); } catch (e) { /* not encoded, that's fine */ }
return encodeURIComponent(str);
}
2. + dans les segments de chemin
Un dev encode un nom de fichier avec une bibliothèque qui utilise + pour les espaces. my report.pdf devient my+report.pdf. Le serveur interprète + comme un signe plus littéral et renvoie une 404.
Retenez : + signifie espace uniquement dans les chaînes de requête (après ?). Dans les chemins, + reste +. Utilisez %20 pour les espaces dans les segments de chemin.
3. URI de redirection OAuth cassées
L’URL d’autorisation ressemble à ceci :
https://auth.provider.com/authorize?redirect_uri=https://myapp.com/callback?code=abc&state=xyz
Le serveur OAuth lit redirect_uri=https://myapp.com/callback?code=abc et traite state=xyz comme un paramètre de premier niveau. L’authentification échoue.
Correction : encodez toute la valeur du redirect URI :
const redirectUri = 'https://myapp.com/callback?code=abc&state=xyz';
const authUrl = `https://auth.provider.com/authorize?redirect_uri=${encodeURIComponent(redirectUri)}`;
// redirect_uri=https%3A%2F%2Fmyapp.com%2Fcallback%3Fcode%3Dabc%26state%3Dxyz
4. Texte non-ASCII illisible dans les journaux
Vos logs affichent %E4%BD%A0%E5%A5%BD au lieu de caractères chinois lisibles. Pas de panique — l’URL est correctement encodée. C’est votre visionneuse de logs qui ne décode pas le percent-encoding.
Correction : passez les logs dans un décodeur, ou collez l’URL dans un décodeur d’URL pour lire le texte original.
5. Échecs de signature d’API
OAuth 1.0 et AWS Signature V4 exigent un encodage RFC 3986 strict. Or encodeURIComponent() en JavaScript laisse passer !, ', (, ) et *. Si ces caractères sont dans votre entrée de signature, la signature ne correspondra pas.
Correction : complétez l’encodage :
function rfc3986Encode(str) {
return encodeURIComponent(str).replace(/[!'()*]/g, c =>
'%' + c.charCodeAt(0).toString(16).toUpperCase()
);
}
%20 vs + — Le dilemme de l’encodage des espaces
Deux standards, un seul caractère, une confusion sans fin.
| Standard | L’espace devient | Où ça s’applique |
|---|---|---|
| RFC 3986 (syntaxe URI) | %20 | Partout dans une URL |
application/x-www-form-urlencoded | + | Chaînes de requête issues de soumissions de formulaires HTML |
Le + date des débuts du web. Quand un navigateur soumet un <form> HTML en method="GET", il encode les espaces en + dans la query string. Ce comportement est gravé dans la spec HTML — il ne disparaîtra pas.
Le piège : + ne vaut « espace » que dans les query strings. Dans un chemin, + est un vrai signe plus. https://example.com/my+file.pdf sert un fichier nommé my+file.pdf, pas my file.pdf.
En pratique :
- Utilisez
%20quand vous construisez des URL à la main ou encodez des chemins. Ça marche partout. - Acceptez
+au parsing des query strings de formulaires — votre framework le gère déjà, en général. - Ne mélangez pas. Un seul format par composant.
Encodage d’URL et sécurité
L’encodage d’URL N’EST PAS du chiffrement
Le percent encoding est réversible, déterministe, et sans secret. N’importe qui décode %48%65%6C%6C%6F en Hello en quelques millisecondes.
N’utilisez jamais l’encodage d’URL pour masquer des données sensibles. HTTPS chiffre la requête entière — c’est ça qu’il vous faut. Et gardez en tête que les URL apparaissent dans les logs serveur, l’historique du navigateur et les en-têtes Referer. Les données sensibles vont dans le corps de la requête, pas dans l’URL.
Attaques par redirection ouverte
Un attaquant forge des URL encodées pour contourner les validations naïves. %2F%2Fevil.com se décode en //evil.com — une URL relative au protocole qui pointe vers son domaine.
Défense : validez l’URL décodée, jamais la forme encodée. Maintenez une liste blanche de domaines de redirection autorisés.
Exploits par double encodage
Un WAF vérifie les URL entrantes pour bloquer <script>. L’attaquant envoie %253Cscript%253E — le WAF voit du percent-encoding inoffensif. L’application décode une première fois vers %3Cscript%3E, puis un second décodage produit <script>, et le filtre est contourné.
Défense : décodez intégralement les entrées avant d’appliquer les contrôles de sécurité. Un seul passage de décodage ne suffit pas.
Pour aller plus loin sur la sécurité web, voir notre guide Les essentiels de la sécurité web.
Limites de longueur d’URL et quand l’encodage devient coûteux
La spec HTTP ne fixe pas de longueur maximale pour les URL, mais chaque couche de la pile a ses propres limites.
| Couche | Limite |
|---|---|
| Recommandation générale | 2 000 caractères |
| Chrome, Firefox | ~2 Mo (mais les serveurs rejettent bien avant) |
| Apache (par défaut) | 8 190 octets |
| Nginx (par défaut) | 8 192 octets |
| IIS | 16 384 octets (chaîne de requête) |
| CDN, proxys | Variable — souvent 4 096-8 192 octets |
Le percent encoding gonfle les URL. Un caractère chinois passe de 1 à 9 caractères (%E4%B8%AD). Un emoji, à 12. 200 caractères chinois dans la query string produisent 1 800 caractères de percent-encoding — sans compter l’URL de base.
Quand ça déborde : basculez vers POST et envoyez les données dans le corps de la requête. Pour une interface de recherche, un endpoint POST qui accepte un body JSON est la solution propre.
FAQ
Qu’est-ce que l’encodage d’URL et pourquoi les développeurs en ont-ils besoin ?
L’encodage d’URL (percent encoding) convertit les caractères non autorisés dans les URL en séquences %XX. Les URL ne prennent en charge que 66 caractères ASCII non réservés — espaces, esperluettes, texte Unicode et la plupart des signes de ponctuation doivent être encodés pour ne pas casser la structure ou provoquer une mauvaise interprétation côté serveur.
Quelle est la différence entre encodeURI et encodeURIComponent ?
encodeURI() encode une URL complète en préservant les caractères structurels (://, /, ?, &). encodeURIComponent() encode tout sauf A-Z a-z 0-9 - _ . ~ ! ' ( ) *. Prenez encodeURIComponent() pour les valeurs de paramètres. encodeURI() ne sert que quand vous avez une URL complète et voulez corriger les espaces ou le non-ASCII sans casser sa structure.
Pourquoi %20 apparaît-il parfois comme + dans les URL ?
Les deux représentent un espace, mais viennent de standards différents. %20 (RFC 3986) fonctionne partout dans une URL. + (encodage de formulaire HTML) ne fonctionne que dans les query strings. Dans un chemin, + est un signe plus littéral. %20 est toujours sûr ; + persiste par héritage des formulaires HTML.
Comment encoder du texte dans une URL en Python, JavaScript, Go et Java ?
JavaScript : encodeURIComponent('hello world') → hello%20world. Python : urllib.parse.quote('hello world') → hello%20world. Go : url.QueryEscape("hello world") → hello+world. Java : URLEncoder.encode("hello world", UTF_8) → hello+world. Go et Java utilisent l’encodage de formulaire (espace en +) — remplacez + par %20 pour du RFC 3986.
L’encodage d’URL peut-il servir à la sécurité ou au chiffrement ?
Non. L’encodage d’URL est réversible sans clé — n’importe qui le décode en un instant. Aucune confidentialité. Protégez les données sensibles avec HTTPS, pas avec le percent encoding. Les URL passent dans les logs, l’historique navigateur et les en-têtes Referer — les données sensibles vont dans le corps de la requête.
Qu’est-ce que le double encodage et comment le corriger ?
Le double encodage arrive quand une chaîne déjà encodée est encodée une seconde fois. Le % de %20 devient %25, ce qui donne %2520. Le serveur voit le texte %20 au lieu d’un espace. La solution : décoder d’abord, encoder ensuite, une seule fois. Le motif %25 suivi de deux chiffres hex est le signal d’alarme.
Quelle est la longueur maximale d’une URL ?
Pas de maximum officiel dans la spec HTTP, mais 2 000 caractères est la limite sûre en pratique. Apache plafonne par défaut à 8 190 octets, Nginx à 8 192. Les caractères non-ASCII gonflent de 3 à 12 fois une fois percent-encodés, donc les URL internationalisées atteignent ces limites bien plus vite. Au-delà, passez à POST avec un body.