Una vez 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.
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:
// Realizamos la petición y guardamos la promesa
const request = fetch("/robots.txt");
// Si es resuelta, entonces ejecuta esta función...
request.then(function(response) { ... });
El fetch()
devolverá una .then()
. Esto se suele reescribir de la siguiente forma, que queda mucho más simple:
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. A la función fetch(url, options)
se le pasa por parámetro la url
de la petición y, de forma opcional, un objeto options
con opciones de la petición HTTP.
Vamos a examinar un código donde veamos un poco mejor como hacer la petición con fetch
:
// Opciones de la petición (valores por defecto)
const options = {
method: "GET"
};
// Petición HTTP
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... |
body |
Cuerpo de la petición HTTP. Puede ser de varios tipos: String , FormData , Blob , etc... |
headers |
Cabeceras HTTP. Por defecto, {} . |
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)
};
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.
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, a parte 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 es mayor problema, pero en peticiones más complejas utilizar Headers
es una buena práctica.
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:
// Petición HTTP
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.
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 . Equivalente 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.
Lo que vemos a continuación sería un ejemplo un poco más completo de todo lo que hemos visto hasta ahora:
response.ok
response.text()
para procesarlacatch
// Petición HTTP
fetch("/robots.txt")
.then(response => {
if (response.ok)
return response.text()
else
throw new Error(response.status);
})
.then(data => {
console.log("Datos: " + data);
})
.catch(err => {
console.error("ERROR: ", err.message)
});
De hecho, 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.
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:
request(url)
que definimos con async
fetch
utilizando await
para esperar y resolver la promesaresponse.ok
response.text()
utilizando await
y devolvemos el resultadoconst 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
. Al final, utilizar .then()
o async/await
es una cuestión de gustos y puedes utilizar el que más te guste.
Soy Manz, vivo en Tenerife (España) y soy streamer partner en Twitch y profesor. Me apasiona el universo de la programación web, el diseño y desarrollo web y la tecnología en general. Aunque soy full-stack, mi pasión es el front-end, la terminal y crear cosas divertidas y locas.
Puedes encontrar más sobre mi en Manz.dev