Una vez que aprendemos a realizar peticiones HTTP mediante XHR nos damos cuenta que es un mecanismo muy interesante y útil, y que nos abre un mundo de posibilidades trabajando con Javascript. Sin embargo, con el tiempo nos vamos dando cuenta también, que la forma de trabajar con objetos XMLHttpRequest
, aunque es muy potente requiere mucho trabajo que hace que el código no sea tan legible y práctico como quizás debería.
Entonces es cuando surge fetch
, un sistema más moderno, basado en promesas de Javascript, para realizar peticiones HTTP asíncronas de una forma más legible y cómoda.
Peticiones con el método fetch()
Fetch es el nombre de una nueva API para Javascript con la cuál podemos realizar peticiones HTTP asíncronas utilizando promesas y de forma que el código sea un poco más sencillo y menos verbose. La forma de realizar una petición es muy sencilla, básicamente se trata de llamar a fetch
y pasarle por parámetro la URL de la petición a realizar:
const promise = fetch("/robots.txt");
promise.then(function(response) {
/* ... */
});
El fetch()
devolverá una
El modo más habitual de manejar las promesas es utilizando .then()
, aunque también se puede utilizar async/await. Esto se suele reescribir de la siguiente forma, que queda mucho más simple y evitamos constantes o variables temporales de un solo uso:
fetch("/robots.txt")
.then(function(response) {
/** Código que procesa la respuesta **/
});
Al método .then()
se le pasa una función callback donde su parámetro response
es el objeto de respuesta de la petición que hemos realizado. En su interior realizaremos la lógica que queramos hacer con la respuesta a nuestra petición.
Opciones de fetch()
A la función fetch()
, al margen de la url
a la que hacemos petición, se le puede pasar un segundo parámetro de opciones de forma opcional, un
const options = {
method: "GET"
};
fetch("/robots.txt", options)
.then(response => response.text())
.then(data => {
/** Procesar los datos **/
});
Un poco más adelante, veremos como trabajar con la respuesta response
, pero vamos a centrarnos ahora en el parámetro opcional options
de la petición HTTP. En este objeto podemos definir varios detalles:
Campo | Descripción |
---|---|
method | Método HTTP de la petición. Por defecto, GET . Otras opciones: HEAD , POST , etc... |
headers | Cabeceras HTTP. Por defecto, {} . |
body | Cuerpo de la petición HTTP. Puede ser de varios tipos: String , FormData , Blob , etc... |
credentials | Modo de credenciales. Por defecto, omit . Otras opciones: same-origin e include . |
Lo primero, y más habitual, suele ser indicar el método HTTP a realizar en la petición. Por defecto, se realizará un GET
, pero podemos cambiarlos a HEAD
, POST
, PUT
o cualquier otro tipo de método. En segundo lugar, podemos indicar objetos para enviar en el body
de la petición, así como modificar las cabeceras en el campo headers
:
const options = {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(jsonData)
};
En este ejemplo, estamos enviando una petición POST
, indicando en la cabecera que se envía contenido JSON y en el cuerpo de los datos, enviando el objeto jsonData
, codificado como texto mediante stringify()
.
Por último, el campo credentials
permite modificar el modo en el que se realiza la petición. Por defecto, el valor omit
hace que no se incluyan credenciales en la petición, pero es posible indicar los valores same-origin
, que incluye las credenciales si estamos sobre el mismo dominio, o include
que incluye las credenciales incluso en peticiones a otros dominios.
Recuerda que estamos realizando peticiones relativas al mismo dominio. En el caso de realizar peticiones a dominios diferentes obtendríamos un problema de CORS (Cross-Origin Resource Sharing) similar al siguiente:
Access to fetch at 'https://otherdomain.com/file.json' from origin 'https://domain.com/' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
Más adelante hablaremos de CORS y de como solucionar estos problemas si necesitamos realizar peticiones a otros dominios.
Las cabeceras Headers
Aunque en el ejemplo anterior hemos creado las cabeceras como un Headers
con el que trabajar:
const headers = new Headers();
headers.set("Content-Type", "application/json");
headers.set("Content-Encoding", "br");
Para ello, aparte del método .set()
podemos utilizar varios otros para trabajar con cabeceras, comprobar su existencia, obtener o cambiar los valores o incluso eliminarlos:
Método | Descripción |
---|---|
.has( name) | Comprueba si la cabecera name está definida. |
.get( name) | Obtiene el valor de la cabecera name . |
.set( name, value) | Establece o modifica el valor value a la cabecera name . |
.append( name, value) | Añade un nuevo valor value a la cabecera name . |
.delete( name) | Elimina la cabecera name . |
Como muchos otros objetos iterables, podemos utilizar los métodos .entries()
, .keys()
y/o .values()
para recorrerlos:
for ([key, value] of headers.entries()) {
console.log("Clave: ", key, "valor: ", value);
}
Para peticiones con pocas cabeceras no hay mayor problema, pero en peticiones más complejas utilizar Headers
es una buena práctica.
La respuesta Response
Si volvemos a nuestro ejemplo de la petición con fetch
, observaremos que en el primer .then()
tenemos un objeto response
. Se trata de la respuesta que nos llega del servidor web al momento de recibir nuestra petición:
fetch("/robots.txt", options)
.then(response => response.text())
.then(data => {
/** Procesar los datos **/
});
Aunque en este ejemplo, simplemente estamos utilizando una arrow function que hace un return
implícito de la promesa que devuelve el método .text()
, dicho objeto response
tiene una serie de propiedades y métodos que pueden resultarnos útiles al implementar nuestro código.
Por el lado de las propiedades, tenemos las siguientes:
Propiedad | Descripción |
---|---|
.status | Código de error HTTP de la respuesta (100-599). |
.statusText | Texto representativo del código de error HTTP anterior. |
.ok | Devuelve true si el código HTTP es 200 (o empieza por 2 ). |
.headers | Cabeceras de la respuesta. |
.url | URL de la petición HTTP. |
Si venimos de XMLHttpRequest
, esto no nos resultará nada extraño. Las propiedades .status
y statusText
nos devuelven el código de error HTTP de la respuesta en formato numérico y cadena de texto respectivamente.
Sin embargo, existe una novedad respecto a XHR, y es que tenemos una propiedad .ok
que nos devuelve true
si el código de error de la respuesta es un valor del rango 2xx
, es decir, que todo ha ido correctamente. Así pues, tenemos una forma práctica y sencilla de comprobar si todo ha ido bien al realizar la petición:
fetch("/robots.txt")
.then(response => {
if (response.ok)
return response.text()
})
Por último, tenemos la propiedad .headers
que nos devuelve las cabeceras de la respuesta y la propiedad .url
que nos devuelve la URL completa de la petición que hemos realizado.
Procesando la respuesta
Por otra parte, la instancia response
también tiene algunos métodos interesantes, la mayoría de ellos para procesar mediante una promesa los datos recibidos y facilitar el trabajo con ellos:
Método | Descripción |
---|---|
.text() | Devuelve una promesa con el texto plano de la respuesta. |
.json() | Idem, pero con un objeto json . Equivale a usar JSON.parse() . |
.blob() | Idem, pero con un objeto Blob (binary large object). |
.arrayBuffer() | Idem, pero con un objeto ArrayBuffer (buffer binario puro). |
.formData() | Idem, pero con un objeto FormData (datos de formulario). |
.clone() | Crea y devuelve un clon de la instancia en cuestión. |
Response.error() | Devuelve un nuevo objeto Response con un error de red asociado. |
Response.redirect(url, code) | Redirige a una url , opcionalmente con un code de error. |
Observa que en los ejemplos anteriores hemos utilizado response.text()
. Este método indica que queremos procesar la respuesta como datos textuales, por lo que dicho método devolverá una
fetch("/robots.txt")
.then(response => response.text())
.then(data => console.log(data));
Observa que en este fragmento de código, tras procesar la respuesta con response.text()
, devolvemos una .then()
, donde gestionamos dicho contenido almacenado en data
.
Ten en cuenta que tenemos varios métodos similares para procesar las respuestas. Por ejemplo, el caso anterior utilizando el método response.json()
en lugar de response.text()
sería equivalente al siguiente fragmento:
fetch("/contents.json")
.then(response => response.text())
.then(data => {
const json = JSON.parse(data);
console.log(json);
});
Como se puede ver, con response.json()
nos ahorraríamos tener que hacer el JSON.parse()
de forma manual, por lo que el código es algo más directo.
Promesas usando .then()
Lo que vemos a continuación sería un ejemplo un poco más completo de todo lo que hemos visto hasta ahora:
- Comprobamos que la petición es correcta con
response.ok
- Utilizamos
response.text()
para procesarla - En el caso de producirse algún error, lanzamos excepción con el código de error
- Procesamos los datos y los mostramos en la consola
- En el caso de que la
sea rechazada, capturamos el error con el catch
- Si ocurre un error 404, 500 o similar, lanzamos error con
throw
para capturarlo en elcatch
fetch("/robots.txt")
.then(response => {
if (response.ok)
return response.text();
throw new Error(response.status);
})
.then(data => {
console.log("Datos: " + data);
})
.catch(err => {
console.error("ERROR: ", err.message)
});
Podemos refactorizar un poco este ejemplo para hacerlo más legible. Creamos la función isResponseOk()
para procesar la respuesta (invirtiendo el condicional para hacerlo más directo). Luego, los .then()
y .catch()
los utilizamos con una arrow function para simplificarlos:
const isResponseOk = (response) => {
if (!response.ok)
throw new Error(response.status);
return response.text()
}
fetch("/robots.txt")
.then(response => isResponseOk(response))
.then(data => console.log("Datos: ", data))
.catch(err => console.error("ERROR: ", err.message));
Sin embargo, aunque es bastante común trabajar con promesas utilizando .then()
, también podemos hacer uso de async/await
para manejar promesas, de una forma un poco más directa. La única diferencia es que con .then()
el código no es bloqueante, mientras que con async/await
si es bloqueante.
► Más información: Promesas con .then()
(thenables, no bloqueantes)
Promesas usando async/await
Utilizar async/await
no es más que lo que se denomina azúcar sintáctico, es decir, utilizar algo visualmente más agradable, pero que por debajo realiza la misma tarea. Para ello, lo que debemos tener siempre presente es que un await
sólo se puede ejecutar si esta dentro de una función definida como async
.
En este caso, lo que hacemos es lo siguiente:
- Creamos una función
request(url)
que definimos conasync
- Llamamos a
fetch
utilizandoawait
para esperar y resolver la promesa - Comprobamos si todo ha ido bien usando
response.ok
- Llamamos a
response.text()
utilizandoawait
y devolvemos el resultado
const request = async (url) => {
const response = await fetch(url);
if (!response.ok)
throw new Error("WARN", response.status);
const data = await response.text();
return data;
}
const resultOk = await request("/robots.txt");
const resultError = await request("/nonExistentFile.txt");
Una vez hecho esto, podemos llamar a nuestra función request
y almacenar el resultado, usando nuevamente await
.
► Más información: Promesas con async
/await
(bloqueantes)