Skip to content
Powrót do bloga
Poradniki

Kodowanie i dekodowanie URL: przewodnik dewelopera po percent-encoding

Reguły RFC 3986, encodeURI vs encodeURIComponent, mapowanie bajtów UTF-8 i przykłady kodu w JS, Pythonie, Go i Javie. Wypróbuj nasze darmowe narzędzie online.

12 min czytania

Kodowanie i dekodowanie URL: przewodnik dewelopera po percent-encoding

Przeglądasz logi serwera i widzisz w query string coś takiego: %E4%BD%A0%E5%A5%BD. Uszkodzone dane? Bug? Ani jedno, ani drugie — to chińskie znaki 你好, każdy zamieniony na trzy bajty UTF-8, a następnie zakodowany procentowo do formatu bezpiecznego dla URL. Każdy webdeveloper prędzej czy później wpada na tę ścianę: coś wygląda na zepsute, a tymczasem URL działa dokładnie tak, jak został zaprojektowany.

Kodowanie URL — formalnie nazywane percent-encoding — to mechanizm, który sprawia, że znaki specjalne są bezpieczne dla URL-i. Ten przewodnik pokazuje, jak to działa na poziomie bajtów, kiedy sięgnąć po encodeURI, a kiedy po encodeURIComponent, jak poprawnie kodować w czterech językach oraz jakie błędy potrafią zaskoczyć nawet doświadczonych deweloperów.

Wklej dowolny URL w naszym narzędziu Koder i dekoder URL, aby na bieżąco obserwować kodowanie i dekodowanie podczas lektury.

Czym jest kodowanie URL (percent-encoding)?

URL może zawierać tylko niewielki podzbiór znaków ASCII. Litery, cyfry i garstka symboli przechodzą przez internet bez problemu. Wszystko inne — spacje, ampersandy, chiński tekst, emoji — musi zostać zamienione na format, który URL-e są w stanie przenieść.

Percent-encoding zastępuje każdy niebezpieczny bajt znakiem % z dwiema cyframi szesnastkowymi. Spacja staje się %20. Ampersand staje się %26. Nazwa pochodzi właśnie od prefiksu %.

Reguły opisuje RFC 3986, opublikowany w 2005 roku i wciąż obowiązujący. Zastąpił RFC 2396 i doprecyzował, które znaki są bezpieczne, które zarezerwowane oraz jak należy traktować tekst spoza ASCII.

Krótkie przykłady:

WejścieZakodowaneDlaczego
hello worldhello%20worldSpacja jest niedozwolona w URL-ach
price=10&tax=2price%3D10%26tax%3D2= i & mają znaczenie strukturalne
%E4%B8%ADSpoza ASCII → bajty UTF-8 → percent-encoded
🚀%F0%9F%9A%80Emoji → 4 bajty UTF-8 → percent-encoded

Które znaki wymagają kodowania?

RFC 3986 dzieli znaki na trzy grupy.

Znaki niezarezerwowane (nigdy nie są kodowane)

Te 66 znaków przechodzi w stanie nienaruszonym w każdej części URL:

A-Z  a-z  0-9  -  .  _  ~

Litery, cyfry, łącznik, kropka, podkreślnik, tylda. Tyle, koniec listy.

Znaki zarezerwowane (zależnie od kontekstu)

Te znaki pełnią rolę strukturalnych separatorów w URL-ach:

ZnakRola w strukturze URL
:Oddziela schemat od authority (https:)
/Oddziela segmenty ścieżki
?Rozpoczyna query string
#Rozpoczyna fragment
&Oddziela parametry w query string
=Oddziela klucz parametru od wartości
@Oddziela userinfo od hosta
+ ! $ ' ( ) * , ; [ ]Różne role zarezerwowane

Reguła: gdy znak zarezerwowany pełni swoją funkcję strukturalną, należy zostawić go w spokoju. Gdy występuje jako dane (np. wewnątrz wartości parametru), trzeba go zakodować.

Wszystko inne (zawsze kodowane)

Spacje, nawiasy ostre, klamry, znaki potoku, ukośniki wsteczne oraz znaki spoza ASCII (chiński, arabski, emoji) muszą zostać zakodowane procentowo.

Jedna komplikacja: RFC 3986 koduje spacje jako %20, ale wysyłka formularzy HTML używa +. Więcej o tym konflikcie w dalszej części.

Jak naprawdę działa kodowanie URL: pipeline UTF-8

Dla znaków ASCII kodowanie jest proste: wystarczy odczytać wartość bajtu w hex i poprzedzić ją znakiem %. Spacja (wartość bajtu 32, hex 20) staje się %20.

Dla tekstu spoza ASCII kodowanie składa się z trzech kroków:

Krok 1 — znak na unikodowy code point. Znak é jest mapowany do code pointu U+00E9. Emoji 🚀 mapuje się na U+1F680.

Krok 2 — code point na bajty UTF-8. UTF-8 używa od 1 do 4 bajtów w zależności od zakresu code pointu. é (U+00E9) zamienia się na dwa bajty: 0xC3 0xA9. Emoji rakiety (U+1F680) zamienia się na cztery bajty: 0xF0 0x9F 0x9A 0x80.

Krok 3 — każdy bajt na %XX. Każdy bajt z kroku 2 dostaje własny tryplet w formacie percent-encoded.

Oto pełny pipeline dla różnych typów znaków:

ZnakCode pointBajty UTF-8ZakodowaneMnożnik rozmiaru
AU+004141A (bez kodowania)
spacjaU+002020%20
éU+00E9C3 A9%C3%A9
U+4E2DE4 B8 AD%E4%B8%AD
🚀U+1F680F0 9F 9A 80%F0%9F%9A%8012×

Można to samodzielnie zweryfikować w JavaScripcie:

const char = '中';
const encoded = encodeURIComponent(char);
console.log(encoded); // '%E4%B8%AD'

// Prześledź bajty
const bytes = new TextEncoder().encode(char);
console.log([...bytes].map(b => '%' + b.toString(16).toUpperCase()).join(''));
// '%E4%B8%AD' — zgadza się

Ta ekspansja ma znaczenie przy limitach długości URL. URL z 20 chińskimi znakami dorzuca 180 znaków tekstu zakodowanego procentowo.

encodeURI vs encodeURIComponent — jak wybrać właściwą funkcję

Te dwie funkcje JavaScript są nieustannie mylone. Wyglądają podobnie, ale kodują zupełnie różne zestawy znaków.

encodeURI()encodeURIComponent()
CelZakodować kompletny URLZakodować pojedynczy komponent (klucz lub wartość parametru)
Zachowuje: / ? # & = @ + $ ,Żadnego z nich
KodujeSpacje, znaki spoza ASCII, część interpunkcjiWszystko poza A-Z a-z 0-9 - _ . ~ ! ' ( ) *
Kiedy używaćGdy mamy pełny URL ze spacjami lub znakami Unicode w ścieżceGdy budujemy parametry query z danych od użytkownika

Bug, który regularnie trafia na produkcję:

// ❌ BUG: encodeURI NIE koduje &
const search = 'Tom & Jerry';
const bad = `https://api.example.com/search?q=${encodeURI(search)}`;
// Wynik: https://api.example.com/search?q=Tom%20&%20Jerry
// Znak & rozcina query string — serwer widzi q=Tom%20 i osobny parametr %20Jerry

// ✅ POPRAWKA: encodeURIComponent koduje & jako %26
const good = `https://api.example.com/search?q=${encodeURIComponent(search)}`;
// Wynik: https://api.example.com/search?q=Tom%20%26%20Jerry

W razie wątpliwości warto wybrać encodeURIComponent(). To poprawny wybór w 95% rzeczywistych scenariuszy budowania URL.

Wypróbuj oba tryby obok siebie w naszym narzędziu Koder URL →

Kodowanie URL w każdym języku

JavaScript (przeglądarka i Node.js)

// Zakoduj wartość parametru
const value = encodeURIComponent('price >= 100 & currency = €');
// 'price%20%3E%3D%20100%20%26%20currency%20%3D%20%E2%82%AC'

// Zdekoduj
const original = decodeURIComponent(value);
// 'price >= 100 & currency = €'

// Nowoczesne podejście: URLSearchParams obsługuje kodowanie automatycznie
const params = new URLSearchParams({ q: 'hello world', lang: '中文' });
console.log(params.toString());
// 'q=hello+world&lang=%E4%B8%AD%E6%96%87'
// Uwaga: URLSearchParams używa + dla spacji (form encoding)

Python

from urllib.parse import quote, unquote, urlencode

# Zakoduj segment ścieżki
quote('hello world/file name.txt', safe='/')
# 'hello%20world/file%20name.txt'

# Zakoduj parametry query
urlencode({'q': '你好', 'page': '1'})
# 'q=%E4%BD%A0%E5%A5%BD&page=1'

# quote_plus używa + dla spacji (form encoding)
from urllib.parse import quote_plus
quote_plus('hello world')  # 'hello+world'
quote('hello world')       # 'hello%20world'

Go

import "net/url"

// Zakoduj wartość query (używa + dla spacji)
url.QueryEscape("hello world & more")
// "hello+world+%26+more"

// Zakoduj segment ścieżki (używa %20 dla spacji)
url.PathEscape("hello world & more")
// "hello%20world%20&%20more"

// Bezpieczne budowanie URL z 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;

// Kodowanie (używa + dla spacji — Java trzyma się form encoding)
String encoded = URLEncoder.encode("hello world & more", StandardCharsets.UTF_8);
// "hello+world+%26+more"

// Dla zgodności z RFC 3986 zamień + na %20
String rfc3986 = encoded.replace("+", "%20");
// "hello%20world%20%26%20more"

// Dekodowanie
String decoded = URLDecoder.decode(encoded, StandardCharsets.UTF_8);
// "hello world & more"

Go i Java domyślnie używają form encoding (spacje jako +). Aby uzyskać wynik zgodny z RFC 3986, należy w post-procesie zamienić + na %20.

Pięć błędów w kodowaniu URL, które psują produkcję

1. Podwójne kodowanie (%2520 zamiast %20)

Kodujesz string. Framework koduje go ponownie. Znak % w %20 staje się %25, a serwer widzi dosłowny tekst %20 zamiast spacji.

Symptom: URL-e zawierają %2520, %253D lub inne wzorce %25xx.

Diagnoza: %25 w URL oznacza, że znak % został zakodowany — zwykle wskazuje to na podwójne kodowanie.

Naprawa: najpierw zdekodować, potem zakodować raz. Warto sprawdzić, czy wejście nie jest już zakodowane, zanim zostanie zakodowane ponownie.

// Wykryj podwójne kodowanie
function isDoubleEncoded(str) {
  return /%25[0-9A-Fa-f]{2}/.test(str);
}

// Bezpieczne kodowanie: najpierw dekoduj, potem zakoduj
function safeEncode(str) {
  try { str = decodeURIComponent(str); } catch (e) { /* nie było zakodowane, w porządku */ }
  return encodeURIComponent(str);
}

2. + w segmentach ścieżki

Deweloper koduje nazwę pliku biblioteką, która zwraca + dla spacji. Plik my report.pdf staje się my+report.pdf. Serwer traktuje + jako dosłowny plus i zwraca 404.

Reguła: + oznacza spację tylko w query string (po ?). W segmentach ścieżki + to po prostu +. Dla spacji w ścieżkach należy zawsze stosować %20.

3. Zepsute redirect URI w OAuth

URL autoryzacji wygląda tak:

https://auth.provider.com/authorize?redirect_uri=https://myapp.com/callback?code=abc&state=xyz

Serwer OAuth odczytuje redirect_uri=https://myapp.com/callback?code=abc i traktuje state=xyz jako osobny parametr najwyższego poziomu. Uwierzytelnianie nie działa.

Naprawa: zakodować całą wartość 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. Zniekształcony tekst spoza ASCII w logach

Logi serwera pokazują %E4%BD%A0%E5%A5%BD zamiast czytelnych chińskich znaków. URL jest poprawnie zakodowany; po prostu twoja przeglądarka logów nie dekoduje sekwencji percent-encoded.

Naprawa: przepuścić logi przez dekoder lub wkleić URL do narzędzia Dekoder URL, aby odczytać oryginalny tekst.

5. Awarie podpisywania API

OAuth 1.0 i AWS Signature V4 wymagają ścisłego kodowania zgodnego z RFC 3986. Funkcja encodeURIComponent() w JavaScripcie nie koduje !, ', (, ) ani *. Jeśli te znaki znajdą się w danych wejściowych do podpisu, sygnatura nie będzie się zgadzać.

Naprawa: post-procesować wynik:

function rfc3986Encode(str) {
  return encodeURIComponent(str).replace(/[!'()*]/g, c =>
    '%' + c.charCodeAt(0).toString(16).toUpperCase()
  );
}

%20 vs + — dylemat kodowania spacji

Dwa standardy nie zgadzają się co do tego, jak zakodować jeden znak.

StandardSpacja staje sięGdzie obowiązuje
RFC 3986 (składnia URI)%20Wszędzie w URL
application/x-www-form-urlencoded+Query string z formularzy HTML

Konwencja + to spuścizna po wczesnych przeglądarkach internetowych. Gdy formularz <form> jest wysyłany metodą method="GET", przeglądarka koduje spacje jako + w query string. Specyfikacja HTML kodyfikuje to zachowanie.

Problem: + oznacza „spację” tylko w query string. W segmentach ścieżki + to dosłowny plus. Dlatego https://example.com/my+file.pdf serwuje plik o nazwie my+file.pdf, a nie my file.pdf.

Wskazówka praktyczna:

  • Stosować %20 przy ręcznym budowaniu URL lub kodowaniu segmentów ścieżki. Działa wszędzie.
  • Akceptować + przy parsowaniu query string z formularzy — twój framework zapewne robi to już sam.
  • Nie mieszać konwencji. Wystarczy wybrać jedną dla danego komponentu i konsekwentnie się jej trzymać.

Kodowanie URL a bezpieczeństwo

Kodowanie URL TO NIE jest szyfrowanie

Percent-encoding to w pełni odwracalna, deterministyczna transformacja bez właściwości kryptograficznych. Każdy może w milisekundach zdekodować %48%65%6C%6C%6F z powrotem do Hello.

Nie należy używać kodowania URL do ukrywania danych wrażliwych. Do szyfrowania całego żądania służy HTTPS. URL-e pojawiają się w logach serwera, historii przeglądarki i nagłówkach Referer, dlatego informacje wrażliwe powinny trafiać do treści żądania, a nie do URL.

Ataki typu open redirect

Atakujący wykorzystują zakodowane URL-e, aby obejść naiwną walidację. Parametr przekierowania zawierający %2F%2Fevil.com dekoduje się do //evil.com, co przeglądarki traktują jako URL względny względem protokołu, wskazujący na domenę atakującego.

Obrona: walidować zdekodowany URL, a nie jego formę zakodowaną. Stosować allowlisty dla domen przekierowań.

Eksploity z podwójnym kodowaniem

WAF sprawdza przychodzące URL-e pod kątem tagów <script>. Atakujący wysyła %253Cscript%253E. WAF widzi tekst zakodowany procentowo i przepuszcza go dalej. Aplikacja dekoduje raz i otrzymuje %3Cscript%3E, kolejne dekodowanie produkuje <script>, omijając filtr.

Obrona: znormalizować wszystkie dane wejściowe (zdekodować w pełni) przed zastosowaniem kontroli bezpieczeństwa. Nie polegać na pojedynczym przebiegu dekodującym.

Więcej o podstawach bezpieczeństwa webowego można znaleźć w naszym przewodniku Najlepsze praktyki bezpieczeństwa webowego.

Limity długości URL i kiedy kodowanie staje się kosztowne

Specyfikacja HTTP nie wyznacza maksymalnej długości URL, ale każda warstwa stosu nakłada własne praktyczne limity.

WarstwaLimit
Ogólna rekomendacja2 000 znaków
Chrome, Firefox~2 MB (ale serwery odrzucają znacznie wcześniej)
Apache (domyślnie)8 190 bajtów
Nginx (domyślnie)8 192 bajty
IIS16 384 bajty (query string)
Sieci CDN, proxyRóżnie — często 4 096–8 192 bajty

Percent-encoding wydłuża URL-e. Pojedynczy chiński znak rośnie z 1 znaku do 9 (%E4%B8%AD). Emoji rozszerza się do 12. Dwieście chińskich znaków w samym query string daje 1 800 znaków tekstu zakodowanego procentowo.

Co zrobić po przekroczeniu limitu: przenieść dane z parametrów query do treści żądania POST. Dla interfejsów wyszukiwania dobrze sprawdza się endpoint POST przyjmujący JSON.

FAQ

Czym jest kodowanie URL i dlaczego deweloperzy go potrzebują?

Kodowanie URL (percent-encoding) zamienia znaki, które nie są dozwolone w URL-ach, na sekwencje hex w postaci %XX. URL-e wspierają tylko 66 niezarezerwowanych znaków ASCII. Spacje, ampersandy, tekst Unicode i większość interpunkcji muszą zostać zakodowane, inaczej zaburzą strukturę URL.

Jaka jest różnica między encodeURI a encodeURIComponent?

encodeURI() koduje pełny URL, zachowując znaki strukturalne, takie jak ://, /, ? i &. encodeURIComponent() koduje wszystko poza A-Z a-z 0-9 - _ . ~ ! ' ( ) *. Do wartości parametrów query lepiej stosować encodeURIComponent(). encodeURI() warto użyć tylko wtedy, gdy mamy kompletny URL i chcemy poprawić w nim spacje albo znaki spoza ASCII bez naruszania struktury.

Dlaczego %20 czasem pojawia się jako + w URL-ach?

Oba reprezentują spację, ale wywodzą się z różnych standardów. %20 zgodne jest z RFC 3986 i działa wszędzie w URL. + pochodzi ze specyfikacji form encoding HTML i działa wyłącznie w query string. W segmentach ścieżki + to dosłowny plus. W razie wątpliwości warto wybrać %20.

Jak zakodować tekst URL-em w Pythonie, JavaScripcie, Go i Javie?

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 i Java domyślnie używają form encoding (spacja jako +) — dla wyniku zgodnego z RFC 3986 należy zamienić + na %20.

Czy kodowania URL można używać do bezpieczeństwa lub szyfrowania?

Nie. Kodowanie URL jest w pełni odwracalne bez żadnego klucza. Nie zapewnia żadnej poufności. Dane wrażliwe trzeba chronić HTTPS, a nie percent-encoding. URL-e pojawiają się w logach serwera, historii przeglądarki i nagłówkach Referer, dlatego dane wrażliwe powinny trafiać do treści żądania.

Czym jest podwójne kodowanie i jak je naprawić?

Podwójne kodowanie pojawia się wtedy, gdy już zakodowany string zostaje zakodowany ponownie. Znak % w %20 zostaje zakodowany jako %25, dając %2520. Serwery widzą wtedy dosłowny tekst %20 zamiast spacji. Naprawa polega na zdekodowaniu wejścia, a następnie jednorazowym zakodowaniu. Wzorzec %25 z dwiema cyframi szesnastkowymi za nim jest sygnałem ostrzegawczym.

Jaka jest maksymalna długość URL?

Oficjalne maksimum nie istnieje w specyfikacji HTTP. 2 000 znaków to bezpieczny limit dla szerokiej kompatybilności. Apache domyślnie obsługuje 8 190 bajtów, Nginx 8 192 bajty. Znaki spoza ASCII rozszerzają się 3–12 razy przy percent-encoding, więc URL-e zinternacjonalizowane szybciej osiągają limity. Dla dużych ładunków lepiej przejść na POST.

Powiązane artykuły

Zobacz wszystkie artykuły