L'utilizzo di XMLHttpRequest
per la gestione di chiamate HTTP da JavaScript risulta abbastanza prolisso e scomodo. È chiaramente questa una delle principali ragioni del successo di metodi alternativi, come ad esempio quello offerto da jQuery tramite
$.ajax()
. Per questo ed altri motivi, il gruppo di lavoro WHATWG ha definito recentemente una alternativa a XMLHttpRequest
: l'API fetch()
.
Rispetto a XMLHttpRequest
, fetch()
ha una sintassi più semplice e meglio integrata nel modello ad oggetti di JavaScript. L'API prevede una gestione delle chiamate asincrone basata sulle promise (che tratteremo meglio più avanti) ed è pensata per essere estesa ed utilizzabile in diversi contesti, non solo all'interno del browser. A questo proposito, dal momento che non tutti i contesti in cui fetch()
è utilizzabile supportano l'API nativamente, può essere opportuno ricorrere ad un polyfill, come ad esempio isomorphic-fetch. Questa libreria è in grado di sopperire al mancato supporto di fetch()
sia lato browser che lato server su Node.js.
Ma andiamo un po' sul concreto e vediamo come utilizzare fetch()
introducendo alcuni dei casi d'uso più comuni. L'utilizzo più semplice dell'API è quello mostrato di seguito:
fetch("http://www.html.it")
.then(response => {
console.log(response);
})
.catch(error => console.log("Si è verificato un errore!"))
Come possiamo vedere, abbiamo specificato l'URL su cui effettuare la richiesta HTTP come parametro della funzione fetch()
ed abbiamo gestito la risposta come una promise. In caso di successo la promise verrà risolta ed entreremo nel ramo then()
, in cui ci verrà fornita la risposta del server sotto forma di oggetto di tipo Response
.
Tra le proprietà più comunemente utilizzate dell'oggetto Response segnaliamo:
Proprietà | Descrizione |
---|---|
status |
È un valore intero che indica il codice di stato HTTP inviato dal server, per esempio 200 in caso di risposta con successo |
statusText |
È una stringa associata al codice di stato, che ne descrive testualmente il significato. Ad esempio, se il codice di risposta è 200, la stringa sarà "OK" |
ok |
È un valore booleano che indica se la risposta del server è stata positiva, cioè se il codice di stato restituito è compreso tra 200 e 299, estremi inclusi |
Occorre sottolineare che la promise restituita dalla funzione fetch()
viene risolta ogni qualvolta c'è una risposta da parte del server, non solo quando otteniamo un codice di stato 200. In altre parole, se entriamo nel ramo then() del codice precedente non dobbiamo dare per scontato di aver ottenuto il contenuto richiesto al server. È buona norma verificare il codice di stato della risposta e gestirlo opportunamente. Il seguente esempio di codice mostra sinteticamente il significato dei diversi codici di stato:
fetch("http://www.html.it").then(response => {
if (response.ok) {
console.log("Contenuto ricevuto");
}
if (response.status >= 100 && response.status < 200) {
console.log("Informazioni per il client");
}
if (response.status >= 300 && response.status < 399) {
console.log("Redirezione");
}
if (response.status >= 400 && response.status < 499) {
console.log("Richiesta errata");
}
if (response.status >= 500 && response.status < 599) {
console.log("Errore sul server");
}
}).catch(error => console.log("Si è verificato un errore!"))
Come possiamo vedere, anche la condizione d'errore sul server (codici di stato compresi tra 500 e 599) determina la risoluzione positiva
della promise generata da fetch()
. Infatti la promise viene rigettata soltanto quando si è verificato
un problema intrinseco nella comunicazione, come ad esempio nel caso in cui il server non risponde o non è disponibile una connessione Internet. In questo caso entreremo nel ramo catch()
della gestione della promise.
Interpretare una risposta HTTP
Una volta inviata la richiesta al server, con molta probabilità vorremo leggere il contenuto della risposta. L'oggetto Response
ci mette a disposizione alcuni metodi per ottenere il contenuto restituito dal server in base al tipo:
Metodo | Descrizione |
---|---|
text() |
Restituisce il contenuto sotto forma di testo |
json() |
Effettua il parsing del contenuto e lo restituisce sotto forma di oggetto |
blob() |
Restituisce il contenuto sotto forma di dati non strutturati (blob) |
arrayBuffer() |
Restituisce il contenuto strutturato in un arrayBuffer |
Ciascuno di questi metodi restituisce una promise, quindi se ad esempio ci aspettiamo un JSON come risposta ad una richiesta HTTP, potremo ottenere l'oggetto deserializzato come nel seguente esempio:
fetch("https://www.html.it/api/articoli/123").then(response => {
if (response.ok) {
return response.json();
}
}).then(articolo => console.log(articolo.titolo)).catch(error => console.log("Si è verificato un errore!"))
Nell'esempio abbiamo supposto che esista una API che restituisca un articolo e, una volta ricevuta la risposta dal server, mostriamo sulla console il titolo dell'articolo.
Invio di dati al server
L'esempio di richiesta HTTP che abbiamo presentato fin qui rappresenta la forma più semplice di utilizzo di fetch()
. Non abbiamo specificato un verbo HTTP nè altri parametri, ma soltanto l'URL verso cui intendiamo indirizzare la nostra richiesta. In questo caso è sottinteso l'uso del verbo GET
.
Se intendiamo specificare altri verbi HTTP abbiamo due possibilità. La prima consiste nel creare un oggetto di tipo Request
e passarlo alla funzione fetch()
, come mostrato di seguito:
var richiesta = new Request("https://www.html.it/api/articoli", {
method: "post",
headers: new Headers({
"Content-Type": "application/json"
}),
body: JSON.stringify({
titolo: "Un articolo",
autore: "Mario Rossi"
})
});
fetch(richiesta).then(...).catch(...)
Come possiamo vedere dall'esempio, creiamo l'oggetto Request
specificando l'URL a cui inviare la richiesta HTTP ed un oggetto di opzioni. Le opzioni specificate in questo caso rappresentano il verbo, gli header ed il body.
L'alternativa a questa soluzione consiste nel passare le medesime impostazioni dell'oggetto Request
direttamente a fetch()
:
fetch("https://www.html.it/api/articoli", {
method: "post",
headers: new Headers({
"Content-Type": "application/json"
}),
body: JSON.stringify({
titolo: "Un articolo",
autore: "Mario Rossi"
})
}).then(...).catch(...)
Naturalmente il contenuto del body di una richiesta dipende da come il server si aspetta i dati. Nell'esempio precedente, abbiamo supposto che il server accettasse dati in formato JSON. Se invece fosse previsto l'invio di dati tramite form, potremmo creare un oggetto FormData ed assegnarlo al body, come possiamo vedere nel seguente esempio:
fetch("https://www.html.it/api/articoli", {
method: "post",
headers: new Headers({
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
}),
body: new FormData(document.getElementById("titolo"), document.getElementById("autore"))
}).then(...).catch(...)
Nell'esempio abbiamo supposto che il titolo e l'autore di un articolo fossero contenuti negli omonimi campi di una form HTML. Da notare anche il cambio di header che specifica il tipo di contenuto inviato tramite POST.
Gestire gli header
Negli esempi di invio di dati al server tramite POST
, abbiamo specificato l'header Content-Type
per informare il server sul formato dei dati che stiamo inviando. Come abbiamo potuto vedere, per la creazione degli header abbiamo fatto ricorso al costruttore Headers()
. Possiamo creare una collezione di header passando al costruttore un oggetto che mappa i nomi degli header ai rispettivi valori, come nel seguente esempio:
var headers = new Headers({
"Content-Type": "application/json",
"Accept": "application/json",
"X-Custom": "Valore per headers custom"
});
In alternativa possiamo aggiungere dinamicamente gli header ad una collezione vuota:
var headers = new Headers();
headers.append("Content-Type", "application/json");
headers.append("Accept", "application/json");
headers.append("X-Custom", "Valore per headers custom");
Oltre al metodo append()
, la collezione degli header prevede alcuni metodi per la loro gestione. Ad esempio, possiamo verificare se nell'insieme è contenuto uno specifico header utilizzando il metodo has()
:
headers.has("Set-Cookie"); //false
headers.has("Content-Type"); //true
Possiamo cambiare il valore di un header tramite il metodo set()
:
headers.set("Content-Type", "text/plain");
Per ottenere il valore di uno specifico header faremo uso di get()
:
console.log(headers.get("Accept")); //application/json
Per eliminare un header useremo il metodo delete()
:
headers.delete("X-Custom");
Dal momento che il significato degli header determina la comunicazione tra client e server, esistono delle limitazioni alla loro gestione. Queste limitazioni sono determinate da una proprietà interna della collezione (guard
) il cui valore, non gestibile da JavaScript, stabilisce quali dei metodi visti possono essere utilizzati. Ad esempio, se riceviamo una risposta HTTP e vogliamo analizzare gli header ricevuti, non possiamo cambiare gli header a nostro piacimento, come si tenta di fare nel seguente esempio:
fetch("https://www.html.it/api/articoli/123").then(response => {
response.headers.set("Content-Length", "150");
}).catch(error => console.log("Si è verificato un errore!"))
Naturalmente il tentativo di assegnare una dimensione del contenuto inviato dal server diversa da quella originale genererà un'eccezione.