Skip to content

Guide

URL Encoding Spiegato: percent-encoding, query string e le insidie

Metà dei bug nella gestione degli URL vengono dalla codifica della cosa sbagliata — o dalla doppia codifica.

By Published

La codifica URL è uno di quei temi che sembra banale finché la produzione non inizia a restituire 400 Bad Request su ogni ricerca che contiene un accento francese. Le regole sono semplici, ma sono distribuite tra quattro specifiche che non concordano completamente, e le funzioni di supporto JavaScript hanno nomi che ingannano attivamente. Questa guida raccoglie le regole in un unico posto e contrassegna i punti in cui gli standard discordano silenziosamente.

Cosa significa davvero “URL encoding”

Gli URL sono limitati a un piccolo sottoinsieme di ASCII. Qualsiasi carattere al di fuori di quel set — o qualsiasi carattere che ha un significato strutturale che il parser altrimenti interpreterebbe — deve essere rappresentato come uno o più byte preceduti da %. Questo è il percent-encoding, e le regole su cosa codificare provengono da RFC 3986. Potete provare qualsiasi esempio con il nostro encoder URL.

RFC 3986 divide i caratteri in tre gruppi. Il set non riservato è A-Z a-z 0-9 - . _ ~; questi sono sempre sicuri e non richiedono mai la codifica. Il set riservato (: / ? # [ ] @ ! $ & ' ( ) * + , ; =) ha un significato strutturale in qualche parte dell’URL; codificateli quando intendete il carattere letterale, lasciateli intatti quando intendete il delimitatore. Tutto il resto — inclusi spazio, ", <, >,{ e tutto il non-ASCII — deve essere percent-encoded.

Dove ogni carattere deve essere codificato

Lo stesso byte può o meno richiedere la codifica a seconda di quale componente dell’URL appare. Un segmento di percorso permette : e @ non codificati; un valore di query non ha bisogno di codificare / o ? (perché il parser si ferma a #); un frammento è il componente più permissivo di tutti. In pratica, quasi tutti ignorano queste distinzioni e codificano qualsiasi cosa al di fuori del set non riservato quando interpolano un valore in un URL. Questo è conservativo e corretto.

L’unico punto in cui questo è importante: non codificate i caratteri / che separano i segmenti del vostro percorso. Codificate ogni segmento, poi uniteli con /. Se codificate anche la barra, ottenete %2F nel percorso, che la maggior parte dei server rifiuterà per ragioni di sicurezza (altrimenti permetterebbe agli attaccanti di contrabbandare sequenze ..oltre i controlli di directory).

encodeURI vs encodeURIComponent in JavaScript

JavaScript include due encoder, e la differenza tra essi è la fonte di un’enorme quantità di tempo di debug sprecato.

  • encodeURI(value) assume che value sia già un URL completo. Lascia intatti i caratteri riservati (; / ? : @ & = + $ , #) in modo che l’URL rimanga analizzabile. Codifica spazi, accenti e il piccolo insieme di caratteri che non sono mai permessi in nessuna parte di un URL.
  • encodeURIComponent(value) assume che value sia un singolo componente che viene inserito in un URL — di solito un segmento di percorso o un valore di query. Codifica tutto tranne il set non riservato più ! ' ( ) *.

La regola generale: se state costruendo un URL per concatenazione, usate encodeURIComponent su ogni valore interpolato. Se state normalizzando un URL già costruito, encodeURIè lo strumento giusto. Non c’è quasi nessun caso in cui vogliate applicare entrambi.

Dati form: l’eccezione +/spazio

Gli invii di form HTML codificano usando il media type application/x-www-form-urlencoded, definito nello standard URL WHATWG. Quel formato diverge da RFC 3986 in due modi. Primo, gli spazi diventano + invece di %20. Secondo, un + letterale nel valore viene codificato come %2B in modo che il decoder possa distinguerli.

Sia %20 che + si decodificano in uno spazio in pratica — ogni parser server-side mainstream accetta entrambi in una query string. Ma gli emettitori dovrebbero essere coerenti. Usate + nei corpi dei form e nelle query string che imitano i corpi dei form; usate %20 altrove, specialmente nei segmenti di percorso dove + è un segno più letterale.

Array e oggetti annidati: non esiste uno standard

Le query string sono state progettate per coppie chiave=valore piatte. Le strutture dati annidate devono essere appiattite, e la regola di appiattimento è una decisione per-framework. Le quattro convenzioni ampiamente usate:

  1. Chiavi ripetute. ?tag=js&tag=tsdiventa un array sul server. La convenzione più semplice; default in Express, Go's net/url e la maggior parte dei parser Node.
  2. Bracket stile PHP. ?tag[]=js&tag[]=ts per gli array; ?user[name]=alex per oggetti annidati. I bracket devono essere percent-encoded (%5B / %5D) per essere strettamente conformi agli standard, sebbene la maggior parte dei server accetti la forma grezza.
  3. Notazione punto Rails. ?user.name=alex&user.age=30. Meno comune oggi; compare in alcune API Ruby e .NET più vecchie.
  4. Separato da virgola. ?tag=js,ts. Comune nelle API REST (OpenAPI lo elenca come stileform/explode=false). Conciso ma ambiguo se i valori stessi possono contenere virgole.

Scegliete una convenzione per API e documentatela. I bug più dolorosi in quest’area provengono da un client che emette bracket parlando con un server che si aspetta chiavi ripetute: entrambi sembrano funzionare, ma uno di essi conserva silenziosamente solo l’ultimo valore.

CJK, emoji e altri non-ASCII

Ogni carattere al di fuori dell’ASCII deve essere codificato come suoi byte UTF-8, poi percent-encoded byte per byte. Il carattere (U+5317) sono i tre byte E5 8C 97 in UTF-8, che diventano %E5%8C%97 in un URL. Le emoji seguono la stessa regola: 🔥 (U+1F525) sono i quattro byte F0 9F 94 A5, codificati come %F0%9F%94%A5.

I sistemi legacy a volte emettono lo stesso carattere in una codifica diversa — GBK per il cinese, Shift-JIS per il giapponese, Windows-1251 per il cirillico. Se state integrando con un backend precedente al 2010 e il vostro testo decodificato risulta incomprensibile, provate a interpretare i byte come Windows-1252 o la codifica nativa del locale prima di assumere che l’URL sia corrotto.

Nomi di dominio internazionalizzati

I nomi host seguono una regola diversa. I nomi host non ASCII vengono codificati come Punycode (RFC 3492), non percent-encoded. Il dominio 例え.jp diventa xn--r8jz45g.jpnell’effettiva query DNS. I browser mostrano la forma Unicode per gli script riconosciuti ma trasmettono la forma Punycode sul wire. Se vi trovate a costruire un URL con un host non-ASCII, convertite l’host con un encoder Punycode, non con l’encoder URL.

Insidie comuni

  • Doppia codifica. La codifica di un valore e poi il passaggio del risultato attraverso un altro encoder produce %2520 invece di %20. Tracciate ogni passaggio di codifica esplicitamente; non “codificate tanto per sicurezza” su un valore che non avete prodotto voi.
  • I frammenti hash sono solo lato client. Tutto ciò che viene dopo # non viene mai inviato al server. Mettere lo stato nel frammento è una scelta deliberata (privacy, routing di app a pagina singola); farlo accidentalmente nasconde i bug finché il server non inizia a dipendere da esso.
  • I segni più nei link mailto. mailto:[email protected] funziona perché gli URL mailto usano le regole RFC 3986, non la codifica dei form. Costruite i link mailto a mano, non con un form-encoder.
  • Il set non riservato esclude storicamente la tilde. RFC 2396 (il predecessore di 3986) trattava ~come riservato. I vecchi encoder emettono ancora %7E; lo standard moderno lo considera non riservato. Entrambi si decodificano ugualmente.
  • I limiti di lunghezza sono reali. La maggior parte dei server limita gli URL a 8 KB. I browser variano da 2 KB (IE precedente) a 32 KB (Chrome moderno). Se la vostra query string si avvicina a quel limite, spostate il payload in un corpo POST.

Un flusso di lavoro pratico

Quando si costruiscono URL nel codice: costruire un oggetto che rappresenti i componenti (schema, host, segmenti di percorso, mappa delle query), poi serializzare una volta con una libreria di fiducia — URL e URLSearchParams nei browser, gli equivalenti in ogni altro linguaggio principale. Gli URL assemblati a mano sono i bug.

Quando si esegue il debug di un URL codificato che qualcuno ha inviato, incollatelo nel nostro URL encoder/decoder e cambiate la direzione. Lo strumento decodifica un clic alla volta, quindi se dovete cliccare due volte per ottenere testo leggibile avete confermato la doppia codifica a monte.

La conclusione onesta

Usate encodeURIComponent su ogni valore interpolato, mai su un URL completo. Abbinate la vostra codifica dei form (spazio-come-più) al vostro contesto di trasporto. Scegliete una convenzione di array per API e documentatela. Fidatevi di UTF-8. E quando qualcosa sembra doppiamente codificato, quasi certamente lo è — tracciate il secondo encoder invece di coprirlo con un passaggio di decodifica.

Frequently asked questions

Devo codificare l'intero URL o solo le sue parti?
Codificate solo le parti che controllate: i segmenti del percorso e i valori delle query. Non codificate mai un intero URL con encodeURIComponent perché danneggerà il separatore dello schema (`://`) e i delimitatori `?` e `&`. Codificate ogni segmento del percorso e ogni valore di query indipendentemente e assemblateli dopo.
Perché il mio URL mostra + invece di %20 per gli spazi?
Questo è il comportamento di `application/x-www-form-urlencoded`, usato dalle invii di form HTML. In quel media type uno spazio viene codificato come `+` e un `+` letterale come `%2B`. Nei percorsi URL RFC 3986, gli spazi sono sempre `%20`. I server di solito decodificano entrambi, ma se costruite un URL a mano, preferite `%20` ovunque eccetto nel corpo dei form.
È mai la scelta giusta usare encodeURI() in JavaScript?
Raramente. `encodeURI()` lascia intatti i caratteri riservati come `?`, `#`, `&` e `/` perché assume che stiate codificando un URL completo. Questo non è quasi mai quello che volete: di solito volete codificare un valore che verrà inserito in un URL, il che è il compito di `encodeURIComponent`. Usate `encodeURI` solo quando sanitizzate un URL letterale digitato dall'utente contenente caratteri non ASCII.
Come vengono codificati i caratteri CJK?
RFC 3986 richiede UTF-8 seguito da percent-encoding di ogni byte. Il carattere `日` (U+65E5) è `E6 97 A5` in UTF-8, quindi si codifica come `%E6%97%A5`. I sistemi più vecchi a volte usavano GBK o Shift-JIS, ecco perché lo scraping di siti CJK legacy occasionalmente produce `%C8%D5` per lo stesso carattere. I browser moderni emettono sempre UTF-8.
Come codifico gli array nelle query string?
Non esiste un unico standard. PHP e Rails usano la notazione bracket (`tags[]=a&tags[]=b`); Express e la maggior parte dei framework Node accettano sia i bracket che le chiavi ripetute (`tags=a&tags=b`); alcune API richiedono valori separati da virgola (`tags=a,b`). Controllate il parser del consumatore prima di scegliere una convenzione e documentate la scelta nel riferimento API.
Perché ottengo valori doppiamente codificati come %2520?
Qualcosa ha codificato il valore, poi qualcos'altro ha codificato il risultato. `%20` (uno spazio codificato) è diventato `%2520` perché il `%` è stato ri-codificato come `%25`. La soluzione è trovare il secondo passaggio di codifica e rimuoverlo — non decodificate e poi ri-codificate come soluzione, perché perderete informazioni per i valori che contenevano legittimamente `%25` prima della codifica.

Related

Published May 31, 2026