URL-Encoding und -Decoding: Percent Encoding im Detail
%E4%BD%A0%E5%A5%BD taucht in deinem Server-Log auf. Sieht nach kaputten Daten aus — sind aber die chinesischen Zeichen 你好, in UTF-8-Bytes zerlegt und prozent-kodiert. Klassiker: Die URL funktioniert, sieht nur komisch aus.
URL-Encoding (Percent Encoding) macht Sonderzeichen URL-sicher. Dieser Artikel zeigt, wie das auf Byte-Ebene abläuft, wann encodeURI und wann encodeURIComponent die richtige Wahl ist, wie du in vier Sprachen korrekt kodierst — und welche Bugs selbst erfahrene Entwickler regelmäßig erwischen.
Wirf eine URL in unseren URL-Dekodierer & -Kodierer und schau dir Encoding und Decoding live an.
Was ist URL-Encoding (Percent Encoding)?
URLs erlauben nur eine Handvoll ASCII-Zeichen. Buchstaben, Ziffern, ein paar Symbole — der Rest muss konvertiert werden: Leerzeichen, &, chinesischer Text, Emoji.
Percent Encoding ersetzt jedes unsichere Byte durch % plus zwei Hex-Ziffern. Leerzeichen wird %20, Kaufmanns-Und wird %26.
RFC 3986 regelt das seit 2005 und gilt bis heute. Er löste RFC 2396 ab: welche Zeichen sicher sind, welche reserviert, wie Nicht-ASCII-Text behandelt wird.
Schnelle Beispiele:
| Eingabe | Kodiert | Warum |
|---|---|---|
hello world | hello%20world | Leerzeichen sind in URLs nicht erlaubt |
price=10&tax=2 | price%3D10%26tax%3D2 | = und & haben strukturelle Bedeutung |
中 | %E4%B8%AD | Nicht-ASCII → UTF-8-Bytes → prozent-kodiert |
🚀 | %F0%9F%9A%80 | Emoji → 4 UTF-8-Bytes → prozent-kodiert |
Welche Zeichen brauchen Encoding?
RFC 3986 teilt Zeichen in drei Gruppen. Die Unterscheidung spart dir stundenlange Debugging-Sessions.
Unreservierte Zeichen (werden nie kodiert)
Diese 66 Zeichen kommen immer unverändert durch:
A-Z a-z 0-9 - . _ ~
Buchstaben, Ziffern, Bindestrich, Punkt, Unterstrich, Tilde. Sonst nichts.
Reservierte Zeichen (kontextabhängig)
Strukturelle Trennzeichen in URLs:
| Zeichen | Rolle in der URL-Struktur |
|---|---|
: | Trennt Schema von Authority (https:) |
/ | Trennt Pfadsegmente |
? | Leitet den Query-String ein |
# | Leitet das Fragment ein |
& | Trennt Query-Parameter |
= | Trennt Parameterschlüssel von Wert |
@ | Trennt Benutzerinfo vom Host |
+ ! $ ' ( ) * , ; [ ] | Verschiedene reservierte Rollen |
Faustregel: Wenn ein reserviertes Zeichen seine strukturelle Aufgabe erfüllt, bleibt es stehen. Sobald es als Daten vorkommt (z. B. in einem Parameterwert), kodieren.
Alles andere (wird immer kodiert)
Leerzeichen, spitze Klammern, geschweifte Klammern, Pipes, Backslashes, Nicht-ASCII-Zeichen — alles kodieren, bevor es in eine URL darf.
Sonderfall Leerzeichen: RFC 3986 sagt %20, HTML-Formularübermittlungen nutzen +. Mehr dazu weiter unten.
URL-Encoding unter der Haube
ASCII-Zeichen sind simpel: Byte-Wert in Hex nachschlagen, % davor. Leerzeichen (Byte 32, Hex 20) wird %20.
Bei Nicht-ASCII geht es in drei Schritten:
Schritt 1 — Zeichen zum Unicode-Codepoint.
é = U+00E9, 🚀 = U+1F680.
Schritt 2 — Codepoint in UTF-8-Bytes.
UTF-8 braucht 1 bis 4 Bytes je nach Codepoint. é (U+00E9) ergibt 0xC3 0xA9. Das Raketen-Emoji (U+1F680) ergibt 0xF0 0x9F 0x9A 0x80.
Schritt 3 — Jedes Byte wird %XX.
Pro Byte ein Triplet.
Die Pipeline im Überblick:
| Zeichen | Codepoint | UTF-8-Bytes | Kodiert | Größenfaktor |
|---|---|---|---|---|
A | U+0041 | 41 | A (nicht kodiert) | 1× |
| Leerzeichen | 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× |
Schnell in JavaScript nachprüfen:
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
Das wird bei URL-Längenbeschränkungen relevant: 20 chinesische Zeichen erzeugen 180 Zeichen prozent-kodierten Text.
encodeURI vs encodeURIComponent — wann welche?
Die beiden zu verwechseln gehört zu den häufigsten URL-Encoding-Fehlern. Ähnlicher Name, völlig verschiedene Zeichensätze.
encodeURI() | encodeURIComponent() | |
|---|---|---|
| Zweck | Komplette URL kodieren | Einzelne Komponente kodieren (Parameterschlüssel oder -wert) |
| Bewahrt | : / ? # & = @ + $ , | Keines davon |
| Kodiert | Leerzeichen, Nicht-ASCII, einige Satzzeichen | Alles außer A-Z a-z 0-9 - _ . ~ ! ' ( ) * |
| Verwenden wenn | Du eine vollständige URL mit Leerzeichen oder Unicode im Pfad hast | Du Query-Parameter aus Benutzereingaben zusammenbaust |
Diesen Bug sieht man regelmäßig in Produktion:
// ❌ 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
Kurzversion: Im Zweifel encodeURIComponent(). Passt in 95 % der Fälle.
Teste beide Modi Seite an Seite in unserem URL-Encoder-Tool →
URL-Encoding in jeder Sprache
JavaScript (Browser & 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 und Java setzen standardmäßig auf Form Encoding (+ für Leerzeichen). Wer RFC-3986-Konformität braucht, ersetzt + durch %20.
Fünf URL-Encoding-Bugs, die Produktion lahmlegen
1. Doppeltes Encoding (%2520 statt %20)
Du kodierst einen String, dein Framework kodiert nochmal. Das % in %20 wird zu %25, und der Server sieht %20 als Text statt als Leerzeichen.
Symptom: %2520, %253D oder andere %25xx-Muster in URLs.
Diagnose: %25 in einer URL deutet fast immer auf doppeltes Encoding hin.
Fix: Erst dekodieren, dann genau einmal kodieren.
// 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. + in Pfadsegmenten
Eine Bibliothek kodiert den Dateinamen my report.pdf als my+report.pdf. Der Server interpretiert + wörtlich — 404.
+ steht nur in Query-Strings (nach ?) für ein Leerzeichen. In Pfaden ist + ein +. Pfade brauchen %20.
3. Fehlerhafte OAuth-Redirect-URIs
Typisches Szenario:
https://auth.provider.com/authorize?redirect_uri=https://myapp.com/callback?code=abc&state=xyz
Der OAuth-Server liest redirect_uri=https://myapp.com/callback?code=abc und interpretiert state=xyz als separaten Top-Level-Parameter. Auth schlägt fehl.
Fix: Den kompletten Redirect-URI kodieren:
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. Verstümmelter Nicht-ASCII-Text in Logs
Server-Logs zeigen %E4%BD%A0%E5%A5%BD statt lesbarer chinesischer Zeichen. Kein Bug — die URL ist korrekt kodiert, nur der Log-Viewer dekodiert nicht.
Fix: Logs durch einen Decoder pipen oder in den URL-Dekodierer werfen.
5. API-Signing-Fehler
OAuth 1.0 und AWS Signature V4 verlangen striktes RFC-3986-Encoding. encodeURIComponent() lässt !, ', (, ) und * unkodiert. Sobald die in der Signing-Eingabe vorkommen, stimmt die Signatur nicht.
Fix: Nachbearbeiten:
function rfc3986Encode(str) {
return encodeURIComponent(str).replace(/[!'()*]/g, c =>
'%' + c.charCodeAt(0).toString(16).toUpperCase()
);
}
%20 vs + — Das Leerzeichen-Encoding-Dilemma
Zwei Standards, ein Zeichen, endlose Verwirrung.
| Standard | Leerzeichen wird zu | Wo es gilt |
|---|---|---|
| RFC 3986 (URI-Syntax) | %20 | Überall in einer URL |
application/x-www-form-urlencoded | + | Query-Strings aus HTML-Formularübermittlungen |
+ stammt aus den Anfangstagen des Web. Browser kodieren Leerzeichen im Query-String als +, wenn ein <form> mit method="GET" abgeschickt wird. Die HTML-Spec schreibt das vor — und das wird sich nicht ändern.
Aber: + heißt nur in Query-Strings „Leerzeichen”. Im Pfad ist + ein wörtliches Plus. https://example.com/my+file.pdf liefert my+file.pdf, nicht my file.pdf.
In der Praxis:
%20nehmen, wenn du URLs von Hand baust oder Pfadsegmente kodierst. Geht immer.+akzeptieren beim Parsen von Query-Strings aus Formularen — dein Framework macht das wahrscheinlich schon.- Nicht mischen. Pro Komponente eine Konvention, fertig.
URL-Encoding und Sicherheit
URL-Encoding ist KEINE Verschlüsselung
Percent Encoding ist vollständig umkehrbar. Kein Schlüssel, kein Geheimnis. %48%65%6C%6C%6F ist in Millisekunden wieder Hello.
Sensible Daten also nie per URL-Encoding „verstecken”. HTTPS verschlüsselt die gesamte Anfrage, aber URLs landen in Server-Logs, Browser-Verlauf und Referer-Headern. Sensible Infos gehören in den Request Body.
Open-Redirect-Angriffe
Angreifer bauen kodierte URLs, die naive Validierungen durchlassen. %2F%2Fevil.com dekodiert zu //evil.com — der Browser behandelt das als protokollrelative URL zur Angreifer-Domain.
Abwehr: Die dekodierte URL validieren, nicht die kodierte. Allowlists für Redirect-Domains nutzen.
Double-Encoding-Exploits
Eine WAF prüft eingehende URLs auf <script>-Tags. Der Angreifer schickt %253Cscript%253E. Die WAF sieht nur prozent-kodierten Text — kein Alarm. Die App dekodiert einmal zu %3Cscript%3E, eine zweite Dekodierung liefert <script>. Filter umgangen.
Abwehr: Eingaben vor Sicherheitsprüfungen vollständig normalisieren (rekursiv dekodieren). Sich nie auf einen einzigen Dekodier-Durchlauf verlassen.
Mehr dazu in unserem Leitfaden Web Security Essentials.
URL-Längenlimits: Wann Encoding teuer wird
Die HTTP-Spec definiert kein Maximum. Jede Schicht im Stack hat aber praktische Grenzen.
| Schicht | Limit |
|---|---|
| Allgemeine Empfehlung | 2.000 Zeichen |
| Chrome, Firefox | ~2 MB (aber Server lehnen weit vorher ab) |
| Apache (Standard) | 8.190 Bytes |
| Nginx (Standard) | 8.192 Bytes |
| IIS | 16.384 Bytes (Query-String) |
| CDNs, Proxies | Variiert — oft 4.096–8.192 Bytes |
Percent Encoding bläht URLs auf. Ein chinesisches Zeichen: 1 Zeichen rein, 9 raus (%E4%B8%AD). Ein Emoji: 12. 200 chinesische Zeichen im Query-String ergeben 1.800 Zeichen kodierten Text — ohne Basis-URL.
Wird es eng: Daten in den Request Body packen und POST nutzen. Suchoberflächen profitieren von einem POST-Endpunkt mit JSON Body.
FAQ
Was ist URL-Encoding und warum brauchen Entwickler es?
URL-Encoding (Percent Encoding) wandelt unerlaubte Zeichen in %XX-Hex-Sequenzen um. URLs unterstützen nur 66 unreservierte ASCII-Zeichen. Leerzeichen, &, Unicode-Text und die meisten Satzzeichen brauchen Encoding, sonst bricht die URL-Struktur oder der Server interpretiert sie falsch.
Was ist der Unterschied zwischen encodeURI und encodeURIComponent?
encodeURI() kodiert eine vollständige URL und lässt Strukturzeichen wie ://, /, ? und & stehen. encodeURIComponent() kodiert alles außer A-Z a-z 0-9 - _ . ~ ! ' ( ) *. Für Query-Parameterwerte: encodeURIComponent(). encodeURI() brauchst du nur, wenn du eine komplette URL hast und Leerzeichen oder Unicode bereinigen willst, ohne die Struktur zu zerstören.
Warum erscheint %20 manchmal als + in URLs?
Beide stehen für ein Leerzeichen, stammen aber aus verschiedenen Standards. %20 folgt RFC 3986, geht überall. + kommt aus der HTML-Form-Encoding-Spec und geht nur in Query-Strings. Im Pfad ist + ein wörtliches Plus. %20 ist immer sicher; + ein Legacy-Relikt.
Wie kodiere ich Text als URL in Python, JavaScript, Go und Java?
JavaScript: encodeURIComponent('hello world') ergibt hello%20world. Python: urllib.parse.quote('hello world') ergibt hello%20world. Go: url.QueryEscape("hello world") ergibt hello+world. Java: URLEncoder.encode("hello world", UTF_8) ergibt hello+world. Go und Java nutzen Form Encoding (+ statt %20) — für RFC-3986-Konformität + durch %20 ersetzen.
Kann URL-Encoding zur Sicherheit oder Verschlüsselung verwendet werden?
Nein. URL-Encoding ist ohne Schlüssel komplett umkehrbar — null Vertraulichkeit. Sensible Daten gehören hinter HTTPS, nicht hinter Percent Encoding. URLs tauchen in Server-Logs, Browser-Verlauf und Referer-Headern auf. Alles Sensible in den Request Body.
Was ist doppeltes Encoding und wie behebe ich es?
Ein bereits kodierter String durchläuft die Kodierung ein zweites Mal. Das % in %20 wird %25, zusammen %2520. Der Server sieht %20 als Text statt als Leerzeichen. Fix: erst dekodieren, dann genau einmal kodieren. %25 gefolgt von zwei Hex-Ziffern verrät das Problem.
Was ist die maximale URL-Länge?
Kein offizielles Maximum in der HTTP-Spec. 2.000 Zeichen sind ein sicherer Richtwert. Apache setzt 8.190 Bytes, Nginx 8.192 Bytes. Nicht-ASCII-Zeichen wachsen beim Encoding um Faktor 3–12, internationalisierte URLs stoßen deshalb schneller ans Limit. Viele Daten? POST mit Request Body.