URL-Encoding: So funktioniert die Prozentkodierung
Beim URL-Encoding wird jedes Byte, das nicht direkt in einer Webadresse stehen darf, durch ein %-Zeichen und den Wert dieses Bytes in Hexadezimalschreibweise ersetzt.
Eine URL darf nur einen kleinen Satz an ASCII-Zeichen enthalten, und mehrere davon haben eine feste Aufgabe: / trennt Pfadsegmente, ? leitet die Query ein, & trennt Parameter und # markiert das Fragment. Sobald ein Wert ein Leerzeichen, einen Umlaut oder eines dieser Zeichen enthält, muss er kodiert werden. URL-Encoding, auch Prozentkodierung oder Percent-Encoding genannt, ist in RFC 3986 definiert. Im Grunde sagt es dem Server: „Dieses Zeichen gehört zum Wert, es ist kein Trennzeichen.“
Das Prinzip: Aus einem Byte werden drei Zeichen
Ein kodiertes Zeichen wird als % geschrieben, gefolgt von zwei Hexadezimalziffern, die den Wert eines Bytes angeben (RFC 3986, Abschnitt 2.1). Das Leerzeichen hat den Code 32, hexadezimal 20, und wird deshalb zu %20. Die Kodierung eines beliebigen Textes erfolgt in drei Schritten:
- Den Text in UTF-8-Bytes umwandeln.
- Bytes, die für nicht reservierte Zeichen stehen, unverändert lassen (ASCII-Buchstaben, Ziffern und
- . _ ~). - Jedes andere Byte durch
%und seinen Hexadezimalwert ersetzen, vorzugsweise in Großbuchstaben.
Ein Zeichen außerhalb von ASCII belegt in UTF-8 mehrere Bytes, deshalb kann aus einem einzigen Buchstaben eine ganze Reihe von Prozentgruppen werden:
| Zeichen | UTF-8-Bytes | Kodierte Form |
|---|---|---|
| Leerzeichen | 20 | %20 (oder + in einem Formular) |
& | 26 | %26 |
= | 3D | %3D |
+ | 2B | %2B |
é | C3 A9 | %C3%A9 |
€ | E2 82 AC | %E2%82%AC |
| 😀 | F0 9F 98 80 | %F0%9F%98%80 |
Aus „café & thé“ wird also caf%C3%A9%20%26%20th%C3%A9. UTF-8 ist keine beliebige Wahl: RFC 3986 empfiehlt es (Abschnitt 2.5), und Browser sowie moderne Programmiersprachen verwenden es. Manche älteren Systeme kodierten Text als Latin-1, wo é ein einzelnes Byte ist und zu %E9 wird. Ein UTF-8-Decoder lehnt diese Sequenz ab: In JavaScript wirft decodeURIComponent('%E9') den Fehler URIError: URI malformed (URI fehlerhaft).
Reservierte und nicht reservierte Zeichen
RFC 3986 teilt die Zeichen in zwei Gruppen ein:
- Nicht reserviert (Abschnitt 2.3):
A-Z a-z 0-9 - . _ ~. Sie müssen nie kodiert werden, und der Standard verlangt sogar, sie nicht zu kodieren:%41undAbedeuten dasselbe. - Reserviert (Abschnitt 2.2): die allgemeinen Trennzeichen
: / ? # [ ] @und die Untertrennzeichen! $ & ' ( ) * + , ; =. Sie geben der URL ihre Struktur.
Die Regel passt in einen Satz: Ein reserviertes Zeichen steht unverändert da, wenn es als Trennzeichen dient, und wird kodiert, wenn es zu den Daten gehört. Vergleichen Sie:
https://example.com/search?q=bread&milk -> q = "bread", dazu ein leerer Parameter "milk"
https://example.com/search?q=bread%26milk -> q = "bread&milk"
Alle anderen Zeichen, also Leerzeichen, Anführungszeichen, % selbst, Umlaute und andere Buchstaben mit Akzent sowie Emojis, müssen überall kodiert werden. Das %-Zeichen wird deshalb als %25 geschrieben: „100 %“ mit Leerzeichen wird zu 100%20%25.
Das Leerzeichen: %20 oder +?
Nach RFC 3986 wird ein Leerzeichen als %20 kodiert. HTML-Formulare verwenden jedoch ein anderes Format, application/x-www-form-urlencoded, bei dem ein Leerzeichen zu + wird und ein echtes Pluszeichen als %2B geschrieben werden muss. Browser nutzen es für Formulare, die per GET gesendet werden, und die meisten Server dekodieren Query-Strings auf diese Weise.
Das führt zu einer häufigen Verwechslung. In einem Pfad ist + einfach ein Pluszeichen. In einem Query-String wird es meist als Leerzeichen gelesen: Eine Suche nach „C++“, die unkodiert als ?q=C++ gesendet wird, kommt beim Server als „C“ mit zwei Leerzeichen an. Korrekt kodiert lautet sie ?q=C%2B%2B.
| Sprache | Leerzeichen als %20 (RFC 3986) | Leerzeichen als + (Formulare) |
|---|---|---|
| JavaScript | encodeURIComponent() | URLSearchParams |
| PHP | rawurlencode() | urlencode(), http_build_query() |
| Python | urllib.parse.quote() | quote_plus(), urlencode() |
In einem Query-String sind beide Formen korrekt, solange die dekodierende Seite dieselbe Konvention anwendet wie die kodierende.
Kodieren in JavaScript: welche Funktion?
JavaScript hat zwei eingebaute Funktionen, die sich darin unterscheiden, welche Zeichen sie unverändert lassen. Für einen einzelnen Wert, ob Query-Parameter oder Pfadsegment, nehmen Sie encodeURIComponent: Es kodiert alles außer A-Z a-z 0-9 - _ . ! ~ * ' ( ). encodeURI geht davon aus, dass es eine vollständige URL mit bereits korrekter Struktur erhält, und lässt deshalb Trennzeichen wie &, / und ? stehen, auch wenn sie zu Ihren Daten gehören. Welche Zeichen jede Funktion genau auslässt und wann encodeURI trotzdem das richtige Werkzeug ist, erklärt der Ratgeber encodeURI vs. encodeURIComponent.
Wenn Sie eine URL aus einzelnen Teilen zusammensetzen, ist es am sichersten, die URL-API jeden Teil an seiner Stelle kodieren zu lassen:
const url = new URL('https://example.com/search');
url.searchParams.set('q', 'café & thé');
url.searchParams.set('sort', 'price');
url.href; // 'https://example.com/search?q=caf%C3%A9+%26+th%C3%A9&sort=price'
Beachten Sie, dass searchParams der Formularkonvention folgt; die Leerzeichen erscheinen deshalb als +.
Beispiele in JavaScript, PHP und Python
Hier derselbe Wert, kodiert und dekodiert in allen drei Sprachen, mit beiden Konventionen für das Leerzeichen:
// JavaScript
encodeURIComponent('café & thé'); // 'caf%C3%A9%20%26%20th%C3%A9'
new URLSearchParams({ q: 'café & thé' }).toString(); // 'q=caf%C3%A9+%26+th%C3%A9'
decodeURIComponent('caf%C3%A9%20%26%20th%C3%A9'); // 'café & thé'
// PHP (Datei als UTF-8 gespeichert)
rawurlencode('café & thé'); // caf%C3%A9%20%26%20th%C3%A9
urlencode('café & thé'); // caf%C3%A9+%26+th%C3%A9
http_build_query(['q' => 'café & thé']); // q=caf%C3%A9+%26+th%C3%A9
urldecode('caf%C3%A9+%26+th%C3%A9'); // café & thé
# Python
from urllib.parse import quote, quote_plus, urlencode, unquote_plus
quote('café & thé') # 'caf%C3%A9%20%26%20th%C3%A9'
quote_plus('café & thé') # 'caf%C3%A9+%26+th%C3%A9'
urlencode({'q': 'café & thé'}) # 'q=caf%C3%A9+%26+th%C3%A9'
unquote_plus('caf%C3%A9+%26+th%C3%A9') # 'café & thé'
Zwei Details sollten Sie kennen. In Python lässt quote() den Schrägstrich standardmäßig unverändert: quote('a/b') gibt a/b zurück. Um einen einzelnen Wert zu kodieren, übergeben Sie safe='', was a%2Fb ergibt. In PHP kodiert urlencode() auch die Tilde als %7E, während rawurlencode() sie so lässt, wie es RFC 3986 verlangt.
Doppelte Kodierung
Wer einen bereits kodierten String noch einmal kodiert, macht aus jedem % ein %25. Aus %20 wird %2520, aus caf%C3%A9 wird caf%25C3%25A9. Bei der Formularkonvention verrät das + das Problem: In PHP gibt urlencode(urlencode('a b')) den Wert a%2Bb zurück, den der Server zu „a+b“ statt „a b“ dekodiert.
Die Ursache ist fast immer dieselbe: Zwei Schichten im Code kodieren denselben Wert, zum Beispiel eine HTTP-Bibliothek, die Parameter kodiert, die Sie bereits selbst kodiert haben. Dieselbe Falle gibt es beim Dekodieren: In PHP enthält $_GET schon dekodierte Werte, und wer darauf urldecode() aufruft, dekodiert ein zweites Mal und verwandelt dabei unter anderem legitime +-Zeichen in Leerzeichen. Die Regel: Kodieren Sie den Rohwert genau einmal, wenn er in die URL kommt, und dekodieren Sie ihn genau einmal beim Lesen.
Nur ein Fall rechtfertigt etwas, das wie doppelte Kodierung aussieht: eine URL, die als Parameter einer anderen URL übergeben wird, etwa in redirect=. Enthält die innere URL bereits %20, steht in ihrer kodierten Form %2520, und das ist korrekt: Der Empfänger dekodiert einmal und erhält die ursprüngliche URL zurück.
Online kodieren und dekodieren
Um einen Wert zu prüfen, ohne Code zu schreiben, bietet der URL Encoder beide Modi, encodeURIComponent und encodeURI, sodass Sie ihre Ergebnisse mit Ihrem eigenen Text vergleichen können. Der URL-Decoder dekodiert schon während der Eingabe und liest + wie ein Formular als Leerzeichen; ein echtes Pluszeichen müssen Sie dort also als %2B eingeben. Alles läuft in Ihrem Browser ab.