URL-encoderen en -decoderen: developer-gids voor percent encoding
Je volgt een server-log en ziet dit in een query-string: %E4%BD%A0%E5%A5%BD. Corrupte data? Een bug? Geen van beide — het zijn de Chinese karakters 你好, elk omgezet naar drie UTF-8-bytes en vervolgens percent-encoded in een URL-veilig formaat. Elke webdeveloper loopt hier vroeg of laat tegenaan: iets ziet er kapot uit, maar de URL werkt precies zoals bedoeld.
URL-encoderen — formeel percent encoding genoemd — is het mechanisme dat speciale karakters veilig maakt voor URL’s. Deze gids behandelt hoe het werkt op byte-niveau, wanneer je encodeURI gebruikt versus encodeURIComponent, hoe je correct encodeert in vier talen en welke bugs zelfs ervaren developers op het verkeerde been zetten.
Plak een willekeurige URL in onze URL-decoder en -encoder om encoderen en decoderen in realtime te zien terwijl je meeleest.
Wat is URL-encoderen (percent encoding)?
Een URL kan maar een kleine subset van ASCII-karakters bevatten. Letters, cijfers en een handjevol symbolen gaan zonder problemen het internet over. Al het andere — spaties, ampersands, Chinese tekst, emoji — moet worden omgezet naar een formaat dat URL’s kunnen dragen.
Percent encoding vervangt elke onveilige byte door een %-teken gevolgd door twee hexadecimale cijfers. Een spatie wordt %20. Een ampersand wordt %26. De naam komt van dat %-prefix.
De regels staan in RFC 3986, gepubliceerd in 2005 en nog steeds de geldende standaard. Het verving RFC 2396 en scherpte de definitie aan van welke karakters veilig zijn, welke gereserveerd en hoe niet-ASCII-tekst moet worden behandeld.
Korte voorbeelden:
| Invoer | Geëncodeerd | Waarom |
|---|---|---|
hello world | hello%20world | Spatie is niet toegestaan in URL’s |
price=10&tax=2 | price%3D10%26tax%3D2 | = en & hebben structurele betekenis |
中 | %E4%B8%AD | Niet-ASCII → UTF-8-bytes → percent-encoded |
🚀 | %F0%9F%9A%80 | Emoji → 4 UTF-8-bytes → percent-encoded |
Welke karakters moeten worden geëncodeerd?
RFC 3986 deelt karakters in drie groepen op.
Ongereserveerde karakters (nooit geëncodeerd)
Deze 66 karakters gaan ongewijzigd door in elk deel van een URL:
A-Z a-z 0-9 - . _ ~
Letters, cijfers, koppelteken, punt, underscore, tilde. Dat is de volledige lijst.
Gereserveerde karakters (contextafhankelijk)
Deze karakters dienen als structurele scheidingstekens in URL’s:
| Karakter | Rol in URL-structuur |
|---|---|
: | Scheidt schema van authority (https:) |
/ | Scheidt pad-segmenten |
? | Start de query-string |
# | Start het fragment |
& | Scheidt query-parameters |
= | Scheidt parameter-sleutel van waarde |
@ | Scheidt userinfo van host |
+ ! $ ' ( ) * , ; [ ] | Diverse gereserveerde rollen |
De regel: als een gereserveerd karakter zijn structurele doel dient, laat het met rust. Als het als data verschijnt (binnen een parameterwaarde bijvoorbeeld), encodeer het dan.
Al het andere (altijd geëncodeerd)
Spaties, hoekhaken, accolades, pipes, backslashes en niet-ASCII-karakters (Chinees, Arabisch, emoji) moeten percent-geëncodeerd worden.
Eén complicatie: RFC 3986 codeert spaties als %20, maar HTML-formulierinzendingen gebruiken +. Meer over dit conflict verderop.
Hoe URL-encoderen echt werkt: de UTF-8-pijplijn
Voor ASCII-karakters is encoderen eenvoudig: zoek de bytewaarde op in hex en zet % ervoor. Een spatie (bytewaarde 32, hex 20) wordt %20.
Voor niet-ASCII-tekst bestaat het encoderen uit drie stappen:
Stap 1 — karakter naar Unicode-codepunt.
Het karakter é wordt omgezet naar codepunt U+00E9. De emoji 🚀 wordt omgezet naar U+1F680.
Stap 2 — codepunt naar UTF-8-bytes.
UTF-8 gebruikt 1 tot 4 bytes afhankelijk van het codepunt-bereik. é (U+00E9) wordt twee bytes: 0xC3 0xA9. De raket-emoji (U+1F680) wordt vier bytes: 0xF0 0x9F 0x9A 0x80.
Stap 3 — elke byte naar %XX.
Elke byte uit stap 2 krijgt zijn eigen percent-encoded triplet.
Hier is de volledige pijplijn voor verschillende karaktertypen:
| Karakter | Codepunt | UTF-8-bytes | Geëncodeerd | Groottefactor |
|---|---|---|---|---|
A | U+0041 | 41 | A (niet gecodeerd) | 1× |
| spatie | 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× |
Je kunt dit zelf verifiëren in 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
Deze uitbreiding doet ertoe bij URL-lengtelimieten. Een URL met 20 Chinese karakters voegt 180 karakters percent-geëncodeerde tekst toe.
encodeURI vs encodeURIComponent — de juiste functie kiezen
Deze twee JavaScript-functies worden continu door elkaar gehaald. Ze lijken op elkaar maar encoderen totaal verschillende karaktersets.
encodeURI() | encodeURIComponent() | |
|---|---|---|
| Doel | Een volledige URL encoderen | Één component encoderen (parametersleutel of -waarde) |
| Behoudt | : / ? # & = @ + $ , | Geen enkele hiervan |
| Codeert | Spaties, niet-ASCII, wat leestekens | Alles behalve A-Z a-z 0-9 - _ . ~ ! ' ( ) * |
| Gebruik wanneer | Je een volledige URL hebt met spaties of Unicode in het pad | Je query-parameters bouwt op basis van gebruikersinvoer |
Een bug die regelmatig in productie belandt:
// ❌ 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
Bij twijfel kies je encodeURIComponent(). Het werkt correct in 95% van de praktijkgevallen waarin je URL’s bouwt.
Probeer beide modi naast elkaar in onze URL-encoder →
URL-encoderen in elke taal
JavaScript (browser en 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 en Java gebruiken standaard form encoding (spaties als +). Voor RFC 3986-uitvoer verwerk je het resultaat na door + te vervangen door %20.
Vijf bugs bij URL-encoderen die productie breken
1. Dubbel encoderen (%2520 in plaats van %20)
Je encodeert een string. Een framework encodeert hem opnieuw. De % in %20 wordt %25 en de server ziet letterlijke %20-tekst in plaats van een spatie.
Symptoom: URL’s bevatten %2520, %253D of andere %25xx-patronen.
Diagnose: %25 in een URL betekent dat een %-karakter is gecodeerd, wat meestal wijst op dubbel encoderen.
Oplossing: Decodeer eerst, encodeer daarna één keer. Controleer of de invoer al gecodeerd is voordat je hem encodeert.
// 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 pad-segmenten
Een developer URL-encodeert een bestandsnaam met een bibliotheek die + uitvoert voor spaties. Het bestand my report.pdf wordt my+report.pdf. De server behandelt + als een letterlijk plus-teken en geeft een 404 terug.
De regel: + betekent alleen spatie in query-strings (na ?). In pad-segmenten is + gewoon +. Gebruik altijd %20 voor spaties in paden.
3. Gebroken OAuth redirect-URI’s
De autorisatie-URL ziet er zo uit:
https://auth.provider.com/authorize?redirect_uri=https://myapp.com/callback?code=abc&state=xyz
De OAuth-server leest redirect_uri=https://myapp.com/callback?code=abc en behandelt state=xyz als een aparte top-level parameter. Authenticatie mislukt.
Oplossing: Encodeer de volledige redirect-URI-waarde:
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. Onleesbare niet-ASCII-tekst in logs
Server-logs tonen %E4%BD%A0%E5%A5%BD in plaats van leesbare Chinese karakters. De URL is correct gecodeerd; je log-viewer decodeert de percent-geëncodeerde sequenties alleen niet.
Oplossing: Stuur logs door een decoder, of plak de URL in een URL-decoder om de originele tekst te lezen.
5. Falende API-signing
OAuth 1.0 en AWS Signature V4 vereisen strikte RFC 3986-codering. JavaScripts encodeURIComponent() codeert !, ', (, ) of * niet. Als deze karakters in je signing-invoer voorkomen, klopt de handtekening niet.
Oplossing: Verwerk de uitvoer na:
function rfc3986Encode(str) {
return encodeURIComponent(str).replace(/[!'()*]/g, c =>
'%' + c.charCodeAt(0).toString(16).toUpperCase()
);
}
%20 vs + — het dilemma van spatie-encoderen
Twee standaarden zijn het oneens over hoe je één karakter codeert.
| Standaard | Spatie wordt | Waar het van toepassing is |
|---|---|---|
| RFC 3986 (URI-syntaxis) | %20 | Overal in een URL |
application/x-www-form-urlencoded | + | Query-strings uit HTML-formulierinzendingen |
De +-conventie is een overblijfsel uit de begintijd van webbrowsers. Als een <form> verstuurt met method="GET", encodeert de browser spaties als + in de query-string. De HTML-specificatie legt dit gedrag vast.
Het probleem: + betekent alleen “spatie” in query-strings. In pad-segmenten is een + een letterlijk plus-teken. Daarom serveert https://example.com/my+file.pdf een bestand met de naam my+file.pdf, niet my file.pdf.
Praktische richtlijn:
- Gebruik
%20bij het handmatig bouwen van URL’s of het coderen van pad-segmenten. Het werkt overal. - Accepteer
+bij het parsen van query-strings uit formulierinzendingen — je framework regelt dit waarschijnlijk al. - Meng ze niet. Kies één conventie per component en houd je eraan.
URL-encoderen en beveiliging
URL-encoderen is GEEN versleuteling
Percent encoding is een volledig omkeerbare, deterministische transformatie zonder cryptografische eigenschappen. Iedereen kan %48%65%6C%6C%6F binnen milliseconden terug decoderen naar Hello.
Gebruik URL-encoderen niet om gevoelige data te verbergen. Gebruik HTTPS om het hele verzoek te versleutelen. URL’s verschijnen in server-logs, browsergeschiedenis en Referer-headers, dus gevoelige informatie hoort in request-bodies, niet in URL’s.
Open redirect-aanvallen
Aanvallers gebruiken geëncodeerde URL’s om naïeve validatie te omzeilen. Een redirect-parameter met %2F%2Fevil.com decodeert naar //evil.com, wat browsers behandelen als een protocol-relatieve URL die naar het domein van de aanvaller wijst.
Verdediging: Valideer de gedecodeerde URL, niet de geëncodeerde vorm. Gebruik allowlists voor redirect-domeinen.
Dubbel-encoderen-exploits
Een WAF controleert inkomende URL’s op <script>-tags. Een aanvaller stuurt %253Cscript%253E. De WAF ziet percent-encoded tekst en laat het door. De applicatie decodeert één keer naar %3Cscript%3E, een tweede decodering produceert <script>, wat het filter omzeilt.
Verdediging: Normaliseer alle invoer (volledig decoderen) voordat je beveiligingscontroles toepast. Vertrouw niet op één enkele decodeer-stap.
Voor meer over de basis van webbeveiliging, zie onze Web Security Essentials-gids.
URL-lengtelimieten en wanneer encoderen duur wordt
De HTTP-specificatie stelt geen maximum URL-lengte, maar elke laag van de stack legt praktische limieten op.
| Laag | Limiet |
|---|---|
| Algemene aanbeveling | 2.000 karakters |
| Chrome, Firefox | ~2 MB (maar servers weigeren ruim daarvoor al) |
| Apache (standaard) | 8.190 bytes |
| Nginx (standaard) | 8.192 bytes |
| IIS | 16.384 bytes (query-string) |
| CDN’s, proxy’s | Varieert — vaak 4.096-8.192 bytes |
Percent encoding maakt URL’s langer. Eén Chinees karakter groeit van 1 karakter naar 9 (%E4%B8%AD). Een emoji breidt uit naar 12. Tweehonderd Chinese karakters in alleen al een query-string produceren 1.800 karakters percent-encoded tekst.
Als je tegen de limiet aanloopt: Verplaats data van query-parameters naar een POST-request-body. Voor zoekinterfaces werkt een POST-endpoint dat JSON accepteert goed.
FAQ
Wat is URL-encoderen en waarom hebben developers het nodig?
URL-encoderen (percent encoding) zet karakters die niet in URL’s zijn toegestaan om in %XX hex-sequenties. URL’s ondersteunen slechts 66 ongereserveerde ASCII-karakters. Spaties, ampersands, Unicode-tekst en de meeste leestekens moeten worden geëncodeerd of ze breken de URL-structuur.
Wat is het verschil tussen encodeURI en encodeURIComponent?
encodeURI() codeert een volledige URL en behoudt daarbij structurele karakters zoals ://, /, ? en &. encodeURIComponent() codeert alles behalve A-Z a-z 0-9 - _ . ~ ! ' ( ) *. Gebruik encodeURIComponent() voor query-parameterwaarden. Gebruik encodeURI() alleen als je een complete URL hebt en spaties of niet-ASCII-karakters wilt corrigeren zonder de structuur te breken.
Waarom verschijnt %20 soms als + in URL’s?
Beide representeren een spatie, maar ze komen uit verschillende standaarden. %20 volgt RFC 3986 en werkt overal in een URL. + volgt de HTML form-encoding-specificatie en werkt alleen in query-strings. In pad-segmenten is + een letterlijk plus-teken. Gebruik %20 bij twijfel.
Hoe URL-encodeer ik tekst in Python, JavaScript, Go en 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 en Java gebruiken standaard form encoding (spatie als +) — vervang + door %20 voor RFC 3986-uitvoer.
Kun je URL-encoderen gebruiken voor beveiliging of versleuteling?
Nee. URL-encoderen is volledig omkeerbaar zonder sleutel. Het biedt geen enkele vertrouwelijkheid. Bescherm gevoelige data met HTTPS, niet met percent encoding. URL’s verschijnen in server-logs, browsergeschiedenis en Referer-headers, dus gevoelige data hoort in request-bodies.
Wat is dubbel encoderen en hoe los ik het op?
Dubbel encoderen gebeurt als een al geëncodeerde string opnieuw geëncodeerd wordt. De % in %20 wordt gecodeerd als %25, wat %2520 oplevert. Servers zien letterlijke %20-tekst in plaats van een spatie. Los het op door de invoer eerst te decoderen en daarna één keer te encoderen. Het patroon %25 gevolgd door twee hex-cijfers is het verklikkende teken.
Wat is de maximum URL-lengte?
Er bestaat geen officieel maximum in de HTTP-specificatie. 2.000 karakters is de veilige limiet voor brede compatibiliteit. Apache staat standaard op 8.190 bytes, Nginx op 8.192 bytes. Niet-ASCII-karakters breiden 3-12x uit bij percent encoding, dus geïnternationaliseerde URL’s lopen sneller tegen limieten aan. Voor grote payloads stap je over op POST.