+39 3662317539/+34 620899163
info@simonecosci.com

URL e URLSearchParams: costruire e leggere querystring senza concatenare

Sviluppo Siti Web a Tenerife

URL e URLSearchParams: costruire e leggere querystring senza concatenare

Prima o poi, in ogni progetto, compare una riga così:

const url = base + "/cerca?q=" + encodeURIComponent(q) + "&pagina=" + page;

Funziona finché base non finisce con uno slash di troppo, finché non devi aggiungere un
parametro solo in certi casi, e finché nessuno cerca una frase con uno spazio dentro. Il bug è
quasi sempre uno dei due: un secondo ? nella stringa, oppure un carattere codificato nel modo
sbagliato.

Il browser ha due oggetti che fanno esattamente questo lavoro, e Node li espone come globali
dalla versione 10: URL e URLSearchParams. Non serve installare niente.

new URL(path, base): il percorso relativo risolto per te

Il costruttore accetta un URL assoluto, oppure un riferimento relativo più un base su cui
risolverlo. La documentazione è esplicita su un punto che conviene leggere due volte: «When a
base is specified, the resolved URL is not simply a concatenation of url and base»
. Non è
una concatenazione, è la risoluzione dei riferimenti relativi come la fa il browser quando
incontra un href in una pagina:

new URL("/en-US/docs", "https://developer.mozilla.org/fr-FR/toto");
// => 'https://developer.mozilla.org/en-US/docs'

new URL("/a", "https://example.com/?query=1");
// => 'https://example.com/a'

new URL("http://www.example.com");
// => 'http://www.example.com/'

Nel primo caso il percorso del base è stato scartato perché il riferimento parte da /; nel
terzo lo slash finale è comparso da solo, perché un URL normalizzato ha sempre un path. Sono
entrambe cose che la concatenazione non fa.

Se l’URL non è valido, il costruttore solleva un TypeError. Da settembre 2024 esiste
l’alternativa che non lancia niente, URL.parse(), che «returns a newly created URL object»
oppure null se i parametri non si risolvono in un URL valido. Comoda quando l’URL arriva da
fuori e un try/catch intorno a due righe è sproporzionato:

const url = URL.parse(input, "https://example.com");
if (!url) return;

searchParams: leggere la querystring già decodificata

URL espone la querystring come oggetto URLSearchParams in sola lettura sulla proprietà
searchParams, con i valori già decodificati:

const params = new URL("https://example.com/?name=Jonathan%20Smith&age=18").searchParams;
params.get("name"); // "Jonathan Smith"
parseInt(params.get("age"), 10); // 18

Nessun decodeURIComponent() da ricordare. I metodi che servono nel 90% dei casi sono quattro:

  • get(name) restituisce il primo valore associato a quel nome;
  • getAll(name) restituisce tutti i valori, come array;
  • set(name, value) imposta il valore e, se ce n’erano diversi, «deletes the others»;
  • append(name, value) aggiunge una coppia senza toccare quelle che c’erano già.

La differenza tra set e append è l’unica cosa da avere chiara: set è “questo parametro vale
X”, append è “aggiungi anche X”. Per i filtri multipli (?tag=php&tag=docker) serve append, e
si rileggono con getAll.

Anche delete() ha due forme, e la seconda è meno conosciuta: accetta un valore opzionale e
cancella solo le coppie che corrispondono a nome e valore.

const params = new URLSearchParams("foo=1&bar=2&foo=3&foo=1");
params.delete("foo", "1");
params.toString(); // "bar=2&foo=3"

L’iterazione (entries(), keys(), values(), forEach()) segue l’ordine in cui le coppie
appaiono nella querystring. Se ti serve un ordine stabile — per esempio per usare l’URL come
chiave di cache — c’è sort(), che ordina per i code unit UTF-16 delle chiavi ed è stabile, cioè
non cambia l’ordine relativo tra coppie con la stessa chiave:

const searchParams = new URLSearchParams("c=4&a=2&b=3&a=1");
searchParams.sort();
searchParams.toString(); // "a=2&a=1&b=3&c=4"

Nota che a=2 è rimasto prima di a=1.

La trappola: lo spazio diventa +, non %20

Questo è il dettaglio che fa perdere un pomeriggio, e vale la pena saperlo prima.
URLSearchParams usa la codifica application/x-www-form-urlencoded, quella dei form: codifica
tutto tranne alfanumerici ASCII, *, -, . e _, e lo spazio diventa +. La proprietà
search di URL, invece, codifica lo spazio come %20.

Due serializzazioni diverse dello stesso URL. La conseguenza è che una modifica apparentemente
innocua ai searchParams riscrive la querystring, perché — come dice MDN — «When updating this
URLSearchParams, the URL’s search is updated with its serialization»
. L’esempio è loro:

const url = new URL("https://example.com/?a=b ~");
url.href; // "https://example.com/?a=b%20~"
url.searchParams.toString(); // "a=b+%7E"
url.searchParams.sort(); // dovrebbe essere un no-op
url.href; // "https://example.com/?a=b+%7E"

Un sort() su un solo parametro non ha niente da ordinare, e l’URL è cambiato comunque. Lo
stesso vale per una delete() di un parametro che non c’entra:

const url2 = new URL("https://example.com?search=1234&param=my%20param");
url2.search; // "?search=1234&param=my%20param"
url2.searchParams.delete("search");
url2.search; // "?param=my+param"

Non è un bug: + e %20 sono entrambi spazi validi in una querystring, e qualunque server che
legge i parametri come un form li interpreta allo stesso modo. Diventa un problema solo se da
qualche parte confronti le due stringhe — una firma HMAC calcolata sull’URL, un if (url ===
expected)
in un test, una chiave di cache. In quel caso scegli una delle due serializzazioni e
usa sempre quella.

Errori da evitare

Passare un URL intero al costruttore di URLSearchParams. Non lo fa a pezzi: non è un parser
di URL, lavora solo sulla querystring. Un leading ? lo tollera — «strips an initial leading
? off a string, if present»
— ma new URLSearchParams("https://example.com/?q=1") ti dà una
chiave che si chiama https://example.com/?q. Se hai un URL completo, parti da new URL() e usa
.searchParams.

Interpolare un valore che contiene un +. Il costruttore legge il + come uno spazio: se
costruisci la querystring a mano e poi la dai in pasto a URLSearchParams, un + legittimo
(pensa a un numero di telefono) si trasforma in spazio. Usa append() o set() e passa il valore
così com’è, senza codificarlo tu.

Continuare a chiamare encodeURIComponent() sui valori. Se usi set() o append() la
codifica la fa l’oggetto: codificare prima significa codificare due volte, e %20 diventa
%2520. Vale la pena ricordare anche cosa encodeURIComponent() non codifica —
A–Z a–z 0–9 - _ . ! ~ * ' ( ) — mentre la RFC 3986 riserva !, ', (, ) e *. È un’altra
buona ragione per non gestire la codifica a mano.

Distinguere ?foo= da ?foo. Non puoi: URLSearchParams non fa differenza tra i due, in
entrambi i casi il valore è la stringa vuota. Se nella tua API “parametro presente ma vuoto” e
“parametro assente” significano cose diverse, usa has() e cambia il nome del parametro, perché
dalla querystring quella differenza non si recupera.

Due oggetti nativi, zero dipendenze, e un ? di troppo in meno nei log.


Se nel tuo progetto la gestione degli URL è cresciuta per stratificazione e nessuno si fida più a
toccarla, scrivici: guardiamo il codice e ti diciamo cosa vale
la pena sistemare.