XMLHttpRequest
è un oggetto built-in che ci permette di eseguire delle richieste HTTP in JavaScript.
A dispetto del suo nome, contenente il termine “XML”, può funzionare con qualunque tipo di dato, e non solo con il formato XML. Possiamo usarlo per effettuare upload e download di files, tenere traccia dei loro progressi e molto altro ancora.
Tuttavia oggi c’è il più moderno metodo fetch
, che in qualche modo ha soppiantato XMLHttpRequest
.
Nello sviluppo web attuale XMLHttpRequest
viene utilizzato ancora oggi per tre principali ragioni:
- Ragioni storiche: per il supporto degli script già esistenti che fanno ancora uso di
XMLHttpRequest
. - Se abbiamo bisogno di supportare i vecchi browser, e non vogliamo fare uso di polyfills (ad esempio per mantenere gli script snelli).
- Se abbiamo bisogno di fare qualcosa che
fetch
non può ancora fare, ad esempio tenere traccia dei progressi in fase di upload.
Vi suona familiare? Se sì, allora possiamo addentrarci nello studio di XMLHttpRequest
. Altrimenti, potete passare direttamente alla sezione Fetch.
Le basi
XMLHttpRequest ha due modalità operative: sincrona e asincrona.
Per prima cosa vediamo la modalità asincrona, dato che è usata nella maggior parte dei casi.
Per fare una richiesta, dividiamo l’operazione in tre fasi:
-
Creiamo
XMLHttpRequest
:let xhr = new XMLHttpRequest();
Il costruttore è privo di argomenti.
-
Lo inizializziamo, solitamente subito dopo
new XMLHttpRequest
:xhr.open(method, URL, [async, user, password])
Questo metodo specifica i parametri principali della richiesta:
method
– metodo HTTP. Solitamente"GET"
o"POST"
.URL
– l’URL della richiesta, una stringa che può anche essere un oggetto URL.async
– se impostato esplicitamente afalse
, la richiesta sarà sincrona, lo affronteremo più avanti.user
,password
– login e password per l’autenticazione HTTP di base (se richiesto).
Nota bene che la chiamata a
open
, contrariamente al suo nome, non apre la connessione. Configura solo la richiesta, ma l’attività di rete comincia solo dopo la chiamata asend
. -
Invio.
xhr.send([body])
Questo metodo apre la connessione ed invia la richiesta al server. Il parametro opzionale
body
contiene il corpo della richiesta.Alcuni metodi, come ad esempio
GET
non supportano il corpo nella richiesta, mentre altri, comePOST
usanobody
per inviare dati al server. Vedremo degli esempi più avanti. -
Ci mettiamo in ascolto sugli eventi
xhr
per la risposta.Questi tre eventi sono quelli utilizzati più di frequente:
load
– quando la richiesta è completa (anche se lo status HTTP è 400 o 500), e la risposta è stata scaricata del tutto.error
– quando la richiesta non può essere espletata, ad esempio per problemi di rete o URL non validi.progress
– viene innescato periodicamente mentre la risposta viene scaricata, e dà informazioni su quanti dati sono stati scaricati.
xhr.onload = function() { alert(`Loaded: ${xhr.status} ${xhr.response}`); }; xhr.onerror = function() { // viene innescato solo se la richiesta non puo' essere eseguita alert(`Network Error`); }; xhr.onprogress = function(event) { // viene scatenato periodicamente // event.loaded - quanti bytes sono stati scaricati // event.lengthComputable = true se il server ha inviato l'header Content-Length // event.total - numero totale di bytes (se lengthComputable è true) alert(`Ricevuti ${event.loaded} su ${event.total}`); };
Ecco un esempio completo. Il seguente codice scarica il contenuto dell’URL /article/xmlhttprequest/example/load
dal server e stampa il progresso di download:
// 1. Crea un nuovo oggetto XMLHttpRequest
let xhr = new XMLHttpRequest();
// 2. Lo configura: richiesta GET per l'URL /article/.../load
xhr.open('GET', '/article/xmlhttprequest/example/load');
// 3. Invia la richiesta alla rete
xhr.send();
// 4. Questo codice viene chiamato dopo la ricezione della risposta
xhr.onload = function() {
if (xhr.status != 200) { // analizza lo status HTTP della risposta
alert(`Error ${xhr.status}: ${xhr.statusText}`); // ad esempio 404: Not Found
} else { // mostra il risultato
alert(`Done, got ${xhr.response.length} bytes`); // response contiene la risposta del server
}
};
xhr.onprogress = function(event) {
if (event.lengthComputable) {
alert(`Received ${event.loaded} of ${event.total} bytes`);
} else {
alert(`Received ${event.loaded} bytes`); // nessun Content-Length
}
};
xhr.onerror = function() {
alert("Request failed");
};
Una volta che il server ha risposto, otteniamo il risultato dentro le seguenti proprietà xhr
:
status
- HTTP status code (un valore numerico):
200
,404
,403
e così via, e può essere0
in caso di fallimento non HTTP. statusText
- messaggio dello status HTTP (una stringa): solitamente
OK
per200
,Not Found
per404
,Forbidden
per403
e via dicendo. response
(vecchi scripts potrebbero usareresponseText
)- La risposta del server.
Possiamo anche specificare un timeout usando la proprietà corrispondente:
xhr.timeout = 10000; // timeout in millisecondi, 10 secondi
Se la richiesta non ha esito nel tempo stabilito, questa viene annullata e viene scatenato l’evento timeout
.
Per aggiungere dei parametri all’URL, come ?name=value
, ed assicurarci che vi sia una corretta codifica, possiamo usare l’oggetto URL:
let url = new URL('https://google.com/search');
url.searchParams.set('q', 'test me!');
// codifica il parametro 'q'
xhr.open('GET', url); // https://google.com/search?q=test+me%21
Response Type
Utilizziamo la proprietà xhr.responseType
per impostare il formato della risposta:
""
(default) – ottenerlo come stringa,"text"
– ottenerlo come stringa,"arraybuffer"
– ottenerlo comeArrayBuffer
(per dati di tipo binario, guardare il capitolo ArrayBuffer, array binari),"blob"
– ottenerlo come unBlob
(per dati binari, guardare Blob),"document"
– ottenerlo come un documento XML (può usare XPath e altri metodi XML) o un documento HTML (basato sul MIME type del dato ricevuto),"json"
– ottiene un JSON (effettua il parsing automaticamente).
Qui ad esempio, otteniamo una risposta in JSON:
let xhr = new XMLHttpRequest();
xhr.open('GET', '/article/xmlhttprequest/example/json');
xhr.responseType = 'json';
xhr.send();
// la risposta e' {"message": "Hello, world!"}
xhr.onload = function() {
let responseObj = xhr.response;
alert(responseObj.message); // Hello, world!
};
Nei vecchi script potremmo imbatterci nelle proprietà xhr.responseText
oppure xhr.responseXML
, che esistono per ragioni storiche, per ottenere sia una stringa, che un documento XML. Oggigiorno, dovremmo impostare il formato dentro la proprietà xhr.responseType
e ottenere xhr.response
come appena illustrato.
Ready states
XMLHttpRequest
modifica lo stato mentre la chiamata progredisce, e lo stato corrente è accessibile tramite xhr.readyState
.
Tutti gli stati, come da specifica sono:
UNSENT = 0; // stato iniziale
OPENED = 1; // chiamata aperta
HEADERS_RECEIVED = 2; // headers della risposta ricevuti
LOADING = 3; // la risposta è in fase di caricamento (è stato già ricevuto un primo pacchetto dati)
DONE = 4; // richiesta completata
Un oggetto XMLHttpRequest
cambia stato durante la chiamata, secondo questo esatto ordine 0
→ 1
→ 2
→ 3
→ … → 3
→ 4
. Lo stato 3
si ripete ad ogni pacchetto ricevuto dalla rete.
Possiamo tenerne traccia tramite l’evento readystatechange
:
xhr.onreadystatechange = function() {
if (xhr.readyState == 3) {
// caricamento
}
if (xhr.readyState == 4) {
// richiesta terminata
}
};
Potremmo trovare listeners per readystatechange
in codice molto vecchio, anche qui per ragioni storiche, in quanto c’era un periodo in cui l’evento load
, e altri eventi, non esistevano ancora. Al giorno d’oggi, i gestori load/error/progress
li hanno deprecati.
Annullamento delle richieste
Possiamo annullare la richiesta in ogni momento. La chiamata a xhr.abort()
è adatta allo scopo:
xhr.abort(); // annulla la richiesta
Ciò scatena l’evento abort
, e xhr.status
diventa 0
.
Richieste sincrone
Se nel metodo open
impostiamo il terzo parametro async
a false
, la richiesta viene eseguita in maniera sincrona.
In altre parole, l’esecuzione del codice JavaScript viene messa in pausa su send()
e si riattiva a risposta ricevuta. Avviene una cosa simile a ciò che succede quando eseguiamo le chiamate ad alert
o prompt
.
Ecco l’esempio precedente riscritto, impostando però il parametro open
a false
:
let xhr = new XMLHttpRequest();
xhr.open('GET', '/article/xmlhttprequest/hello.txt', false);
try {
xhr.send();
if (xhr.status != 200) {
alert(`Error ${xhr.status}: ${xhr.statusText}`);
} else {
alert(xhr.response);
}
} catch(err) { // invece di onerror
alert("Request failed");
}
Potrebbe sembrare un buon codice, ma le chiamate sincrone vengono usate raramente, in quanto bloccano la pagina fino a che la chiamata non ha avuto esito completo. In alcuni browser, diventa persino impossibile eseguire lo scroll della pagina. Se una chiamata sincrona richiedesse troppo tempo, il browser ci suggerirebbe di chiudere la pagina “bloccata”.
Molte capacità avanzate di XMLHttpRequest
, come le richieste da un altro dominio o l’impostazione di un timeout, non sono disponibili se la richiesta è sincrona. Inoltre, non si può avere alcuna indicazione sul progresso del caricamento.
Per i suddetti motivi, le chiamate sincrone sono usate molto raramente, quasi mai, e non affronteremo più argomenti che le coinvolgono direttamente.
Headers HTTP
XMLHttpRequest
permette sia l’invio di headers personalizzati che la loro lettura nelle risposte.
I metodi per gli header HTTP sono 3:
setRequestHeader(name, value)
-
Imposta un header della richiesta con
name
evalue
voluti.Per esempio:
xhr.setRequestHeader('Content-Type', 'application/json');
Limitazioni degli headersMolti headers sono gestiti esclusivamente dal browser, come ad esempio
Referer
edHost
. La lista completa è descritta nelle specifiche.Ad
XMLHttpRequest
non è permesso modificarli, per amore della sicurezza dell’utente ed il mantenimento della correttezza della richiesta.Non può rimuovere un headerUn’altra caratteristica di
XMLHttpRequest
è la sua impossibilità di annullaresetRequestHeader
.Una volta che un header è impostato, resta tale, e qualunque chiamata aggiuntiva non farà altro che aggiungere informazioni all’header stesso, senza sovrascritture.
Per esempio:
xhr.setRequestHeader('X-Auth', '123'); xhr.setRequestHeader('X-Auth', '456'); // l'header diventa: // X-Auth: 123, 456
getResponseHeader(name)
-
Restituisce l’header di risposta con il dato
name
(tranneSet-Cookie
eSet-Cookie2
).Esempio:
xhr.getResponseHeader('Content-Type')
getAllResponseHeaders()
-
Restituisce tutti gli headers di risposta, tranne
Set-Cookie
eSet-Cookie2
.Viene restituito una riga per ogni header presente, ad esempio:
Cache-Control: max-age=31536000 Content-Length: 4260 Content-Type: image/png Date: Sat, 08 Sep 2012 16:53:16 GMT
L’interruzione di riga sarà sempre nella forma
"\r\n"
(indipendentemente dal sistema operativo), in modo che si possa dividerli in headers individuali. Il separatore tra il nome ed il valore è sempre un carattere di due punti seguito da uno spazio": "
. Questo aspetto è ben chiarito nelle specifiche.Quindi, se volessimo ottenere un oggetto con coppie di chiave/valore, dovremmo inserire un po’ di JS.
Come in questo esempio (supponendo che nel caso in cui avessimo due headers con lo stesso nome, il secondo sovrascriverebbe il primo):
let headers = xhr .getAllResponseHeaders() .split('\r\n') .reduce((result, current) => { let [name, value] = current.split(': '); result[name] = value; return result; }, {}); // headers['Content-Type'] = 'image/png'
POST, FormData
Per eseguire una richiesta POST, usiamo l’oggetto built-in FormData.
Ecco la sintassi:
let formData = new FormData([form]); // crea un nuovo oggetto, opzionalmente viene riempito dal <form>
formData.append(name, value); // accoda un campo
Lo creiamo, eventualmente lo riempiamo partendo da un form, e se necessario eseguiamo l’append
di più campi, ed infine:
xhr.open('POST', ...)
– usa il metodoPOST
.xhr.send(formData)
per inviare il form al server.
Esempio:
<form name="person">
<input name="name" value="John">
<input name="surname" value="Smith">
</form>
<script>
// precompila FormData dal form
let formData = new FormData(document.forms.person);
// aggiunge ancora un campo
formData.append("middle", "Lee");
// lo invia
let xhr = new XMLHttpRequest();
xhr.open("POST", "/article/xmlhttprequest/post/user");
xhr.send(formData);
xhr.onload = () => alert(xhr.response);
</script>
Il form viene inviato con la codifica multipart/form-data
.
O, se preferissimo lavorare con JSON, allora lo convertiremmo tramite JSON.stringify
e lo invieremmo come stringa.
Solamente, in questo caso, non dobbiamo dimenticarci di impostare l’header Content-Type: application/json
, perché grazie a questo, molti frameworks server-side saranno in grado di codificare il JSON automaticamente:
let xhr = new XMLHttpRequest();
let json = JSON.stringify({
name: "John",
surname: "Smith"
});
xhr.open("POST", '/submit')
xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');
xhr.send(json);
Il metodo .send(body)
è abbastanza “onnivoro”. Può inviare quasi qualunque tipo di body
, compresi oggetti Blob
e BufferSource
.
Progresso dell’upload
L’evento progress
viene scatenato solamente nella fase di download.
Ossia: se eseguiamo il POST
di qualcosa, come prima cosa XMLHttpRequest
esegue l’upload dei nostri dati (il corpo della richiesta), e successivamente scarica la risposta.
Se facciamo l’upload di qualcosa di grosso, allora sicuramente saremmo più interessati nel tracciare il progresso di upload. Tuttavia xhr.onprogress
non serve ai nostri scopi.
Esiste un altro oggetto per tenere traccia degli eventi di upload, privo di metodi: xhr.upload
.
Genera eventi, in modo simile ad xhr
, con la differenza che xhr.upload
viene scatenato solo durante la fase di upload:
loadstart
– upload cominciato.progress
– viene scatenato periodicamente durante l’upload.abort
– upload annullato.error
– errore non HTTP.load
– upload completato con successo.timeout
– upload scaduto (se la proprietàtimeout
è stata impostata).loadend
– upload completato sia con successo che con errori.
Esempio di gestori:
xhr.upload.onprogress = function(event) {
alert(`Upload di ${event.loaded} su ${event.total} bytes`);
};
xhr.upload.onload = function() {
alert(`Upload completato con successo.`);
};
xhr.upload.onerror = function() {
alert(`Errore durante l'upload: ${xhr.status}`);
};
Ecco un esempio di un caso d’uso reale: upload di file con indicazione del progresso:
<input type="file" onchange="upload(this.files[0])">
<script>
function upload(file) {
let xhr = new XMLHttpRequest();
// tiene traccia del progresso di upload
xhr.upload.onprogress = function(event) {
console.log(`Uploaded ${event.loaded} of ${event.total}`);
};
// completamento del tracciamento: che sia con successo o meno
xhr.onloadend = function() {
if (xhr.status == 200) {
console.log("success");
} else {
console.log("error " + this.status);
}
};
xhr.open("POST", "/article/xmlhttprequest/post/upload");
xhr.send(file);
}
</script>
Richieste cross-origin
XMLHttpRequest
può eseguire delle richieste cross-origin, usando la stessa policy CORS già vista in fetch.
Esattamente come fetch
, non invia cookies ed autorizzazione HTTP verso altre origin di default. Per attivarle, bisogna impostare xhr.withCredentials
a true
:
let xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.open('POST', 'http://anywhere.com/request');
...
Guardare il capitolo Fetch: Cross-Origin Requests per maggiori dettagli riguardanti gli headers cross-origin.
Riepilogo
Codice di esempio per la richiesta GET tramite XMLHttpRequest
:
let xhr = new XMLHttpRequest();
xhr.open('GET', '/my/url');
xhr.send();
xhr.onload = function() {
if (xhr.status != 200) { // errore HTTP?
// gestisce l'errore
alert( 'Error: ' + xhr.status);
return;
}
// ottiene la risposta da xhr.response
};
xhr.onprogress = function(event) {
// informa sul progresso
alert(`Loaded ${event.loaded} of ${event.total}`);
};
xhr.onerror = function() {
// gestisce un errore non HTTP (ad esempio errori di rete)
};
Attualmente ci sono più eventi, la specifica aggiornata li elenca (ordinati secondo il ciclo di vita):
loadstart
– la richiesta è cominciata.progress
– è arrivato un pacchetto della risposta, tutto il corpo della risposta si trova dentroresponse
.abort
– la richiesta è stata annullata tramite la chiamata axhr.abort()
.error
– c’è stato un errore di connessione, ad esempio un nome di domino errato. Non succede per errori HTTP come 404.load
– la richiesta è stata completata con successo.timeout
– la richiesta è stata annullata a causa di un timeout (solo se è stato impostato).loadend
– viene scatenato dopoload
,error
,timeout
oabort
.
Gli eventi error
, abort
, timeout
, e load
sono mutualmente esclusivi. Solamente uno tra questi può essere innescato.
Gli eventi maggiormente usati sono quelli del caricamento avvenuto (load
), del fallimento del caricamento (error
), oppure possiamo usare un singolo gestore loadend
e controllare le proprietà dell’oggetto della richiesta xhr
per vedere come è andata.
Abbiamo incontrato anche un altro evento: readystatechange
. Storicamente, è comparso tanto tempo fa, prima della regolamentazione delle specifiche. Oggigiorno, non è più necessario usarlo, e possiamo sostituirlo con i nuovi eventi, ma può essere spesso trovato in vecchi scripts.
Se dobbiamo tenere traccia degli upload, possiamo metterci in ascolto per i medesimi eventi ma sull’oggetto xhr.upload
.