Tabla de Contenidos
Peticiones asíncronas con la API Fetch
Módulo perteneciente al curso Programación avanzada con JavaScript y ECMAScript.
La API de fetch como alternativa a XHR
Con la aparición de las Promises era hora de que fueran apareciendo nuevas clases y funciones, en definitiva nuevas APIs nativas, que las aprovecharan.
Una de esas APIs es la API de fetch que nos permite tener un sustituto más moderno para XMLHTTPRequest. Así, en lugar de modificar XMLHTTPRequest para añadirle soporte de promises se ha optado por crear una nueva API, más adaptada a las necesidades actuales.
Como se puede ver, dicha API está disponible en cualquier navegador moderno: cualquier versión reciente de Firefox, Chrome, Edge o Safari, así como de los principales navegadores móviles la soporta.
La única excepción, como casi siempre, es Internet Explorer, aunque existen polyfills (el de Github o el de develop.it) para intentar suplir esta carencia.
¿Y qué nos permite hacer esta API? Pues, resumiendo, lo mismo que nos permite XMLHTTPRequest, es decir llamadas HTTP en segundo plano (habilitando así escenarios AJAX), pero de una forma más moderna y sencilla.
Vamos a verlo.
Consideraciones de seguridad previas al uso de fetch
Veamos un ejemplo de uso de la API de fetch para realizar una llamada HTTP en segundo plano. A diferencia de XMLHTTPRequest que se basa en eventos, la API fetch se basa en promises (repasa el módulo dedicado a ellas si tienes dudas).
Para realizar una llamada HTTP asíncrona se usa el método fetch definido en el objeto window (es decir, en el contexto global) del navegador. Eso significa que puedes usar directamente dicha función sin más. De todos modos si quieres comprobar si un navegador soporta fetch verifica si el resultado de fetch in window vale true.
Nota: si usas Node.js, el contexto global de este entorno no implementa este método. Para poder utilizarlo debes instalar algún polyfill específico para Node.js, por ejemplo, a través del paquete NPM node-fetch.
Realizar una petición HTTP
Vamos a ver cómo realizar una petición HTTP asíncronamente usando la función fetch. Para iniciar la petición basta con invocar dicha función con el URL deseado:
let pr = fetch ('https://www.campusmvp.es');
El valor de retorno de fetch es una promise que se resuelve una vez la petición ha terminado. Dado que lo habitual es querer esperar a que la petición termine para realizar alguna acción con el resultado, lo más común será usar el método then de la promise devuelta, así:
fetch ('http://www.campusmvp.es') .then(r => { console.log(r) }). catch (e => { console.error('Error!', e) })
Pruébalo en tu navegador. Ejecuta el código anterior directamente y observa el resultado.
Es más que probable que recibas un error. El error exacto depende del navegador. Por ejemplo, en Chrome el error devuelto es:
Por su parte, en Firefox el error es:
No importa tanto el error concreto sino el hecho de que debes tener claro que fetch sigue todas las convenciones de seguridad modernas. Eso implica, entre otras cosas:
- Evitar cargar elementos no seguros desde orígenes seguros. Es decir, si nuestra página se ha cargado mediante HTTPS, solo podremos cargar recursos servidos vía HTTPS.
- Por defecto
fetchsolo permite peticiones a recursos del mismo origen.
Vamos a habilitar la carga de recursos de otros orígenes, pasando el parámetro mode a cors:
fetch ('https://www.campusmvp.es',{mode: 'cors'}) .then(r => { console.log(r) }). catch (e => { console.error('Error!', e) })
Si lo pruebas ahora, lo más seguro es que… ¡recibas otro error! En la siguiente figura se muestra el error usando Firefox:
La razón de este error se llama CORS y es momento de hablar, brevemente, de él.
CORS
Una explicación exhaustiva de CORS está fuera del alcance de este curso. Básicamente debes saber que CORS es el acrónimo de “Cross Origin Resource Sharing” y especifica un conjunto de reglas que los navegadores deben seguir cuando carguen recursos que provengan de otro origen diferente al de la página actual.
Nota: en terminología web un origen es la ubicación desde la cual se carga un recurso; se trata básicamente de la combinación de protocolo, servidor y puerto. Así, si tenemos una imagen que está en http://mi-servidor.com/images/uno.gif, el origen de dicha imagen es http://mi-servidor.com:80 (80 es el puerto por defecto en HTTP). Una petición entre orígenes (cross-origin request) se da cuando, desde una página que está en un determinado origen (p. ej. el mencionado), accedemos a recursos que están en otro origen (como podría ser http://cdn.mi-servidor.com:80). Desde siempre ha habido etiquetas HTML que soportan llamadas entre orígenes, tales como <image> o <script>. Esas etiquetas tienen sus propias normas y CORS no les influye directamente. CORS afecta a las llamadas realizadas vía JavaScript, es decir, usando básicamente XMLHTTPRequest o bien la API fetch que estamos discutiendo.
CORS define un conjunto de cabeceras que navegador y servidor se intercambian para decidir si desde un determinado origen es posible acceder a los datos de otro origen. Si el cliente o el servidor no envía dichas cabeceras, la petición será denegada. Lo mismo que ocurre cuando el valor de dichas cabeceras no es el correcto.
En el caso de nuestro ejemplo, el servidor no envía las cabeceras CORS cuando se le pide la URL https://www.campusmvp.es. En concreto debería mandar una cabecera, llamada Access-Control-Allow-Origin (ACAO) en la cual se especifica qué orígenes son válidos para dicha respuesta. El cliente (es decir, el navegador) comprueba que el origen esté incluido dentro de esa lista y si no es el caso, deniega la petición.
Una cuestión importante es que, es el navegador quien realiza dicha comprobación una vez obtiene la respuesta del servidor. Eso significa que la petición se ha realizado, pero el cliente (navegador) no nos deja ver los resultados. De hecho, si vamos a la pestaña network de las herramientas de desarrollador vemos la petición:
La petición se ha realizado, la respuesta se ha devuelto (es un 200) pero, al no existir la cabecera ACAO, el navegador nos da ese error.
Puede sorprendernos que una página “moderna”, la web de CampusMVP, no soporte CORS, pero es algo bastante normal. Lo que ocurre es que la URL que estamos usando es la de una página web completa, y raras veces se carga vía AJAX una página web completa. Así que hay muchos servidores que no soportan CORS en URLs que son páginas web completas. Los servidores suelen soportar CORS en aquellos recursos que están pensados para ser usados vía AJAX, básicamente APIs REST.
Peticiones preflight
En el caso de peticiones que modificasen el estado del servidor, el hecho de que CORS se resuelva en el lado del cliente, y no en el servidor, podría representar un problema potencial. Por ejemplo, una petición POST para comprar entradas para un concierto. En este escenario, usando CORS, el navegador realizaría el POST y luego con el resultado verificaría si la cabecera ACAO es correcta. Pero, por su parte, el servidor ya habría procesado la petición POST (y procesado la compra de la entrada) aunque el cliente no pueda ver el resultado de la operación.
Para evitar estos escenarios existe un concepto llamado preflight.
En el caso de peticiones que puedan modificar el estado del servidor (es decir que no se efectúen utilizando el verbo HTTP GET), el cliente realiza otra petición (la petición preflight) usando el verbo OPTIONS. En dicha petición el navegador pasa el origen y el verbo HTTP que se va a usar y espera la respuesta del servidor. Si el servidor entiende CORS, dará una respuesta con la cabecera ACAO establecida. Si el valor de la cabecera ACAO es correcta, solo entonces el cliente (el navegador) realizará la petición inicial.
Así, por ejemplo, tenemos un código que usa fetch para realizar un POST. El navegador, automáticamente antes del POST, enviará una petición OPTIONS al servidor (indicando el origen y estableciendo el valor de Access-Control-Request-Method a POST para indicar que la petición que se quiere realizar es un POST). El servidor responderá con un 200 y la cabecera ACAO al valor apropiado, y el cliente lo podrá verificar antes de hacer la llamada real.
El cliente tan solo realizará la petición POST si el origen que hace la petición está dentro de los valores permitidos por la cabecera ACAO devuelta. En caso contrario devolverá un error de CORS.
En las peticiones GET no se realiza el preflight porque se asume que no pueden modificar el estado del servidor y por lo tanto no es necesaria esa protección adicional.
Uso de la API fetch
Ahora que ya tenemos claras las implicaciones de seguridad a la hora de utilizar fetch, vamos a acceder con esta API a un recurso que tenga soporte para CORS, y que además lo tenga configurado para permitir acceso desde cualquier origen para evitar errores.
Recuerda que CORS solo afecta cuando accedemos a un recurso que se encuentra en otro origen.
Vamos a usar reqres. Se trata de una API REST especialmente pensada para hacer pruebas.
Lo primero que vamos a hacer es acceder al URL “list users” y obtener el resultado que devuelva:
fetch ('https://reqres.in/api/users',{mode: 'cors'}) .then(r => { console.log(r) });
Este código funciona. Si miramos la pestaña de red del navegador, en la respuesta de la petición, encontramos la cabecera ACAO con el valor “*”, que significa que cualquier origen es válido:
Dentro del then el valor de r es el objeto response y contiene varias propiedades y métodos interesantes, entre los que destacan:
status: el código HTTP de estado de la respuesta (200, 404, etc…).ok: nos dice si la respuesta ha sido correcta o no (p. ej. un estado 404 o un 500 se consideran respuestas incorrectas)body: el cuerpo de la respuesta. Se trata de un stream que podemos leer.text(): método que devuelve una promise. Dicha promise se resuelve cuando todo el cuerpo de la respuesta ha sido leído y se resuelve a una cadena con el contenido. Nos permite leer el cuerpo de manera asíncrona.json(): análogo al anterior. Es decir, devuelve una promise que se resuelve cuando se ha leído todo el cuerpo de la respuesta pero, la promise se resuelve a un objeto JavaScript resultado de interpretar la respuesta como un JSON. Es pues ideal para usar en el caso de APIs REST, que suelen devolver JSONs.
En esto se ve que fetch es una API más moderna que XMLHttpRequest: esta última tiene métodos orientados a XML, que era el protocolo en boga cuando dicha API fue diseñada. Posteriormente, la aparición de JSON ha hecho que, al menos para el ecosistema web, el uso de XML se haya reducido mucho, de ahí que fetch soporte nativamente JSON.
Vamos a usar el método json() para obtener el nombre del primer usuario:
fetch ('https://reqres.in/api/users',{mode: 'cors'}) .then(r => r.json()) .then(users => console.log(users.data[0].first_name));
Este código imprime por la consola el nombre del primer usuario, en el momento de probar este código el resultado es George:
Verificar el estado de la respuesta
Usando fetch es siempre importante verificar que la respuesta es correcta, es decir, que el valor de la propiedad ok es true. Es importante porque la promise devuelta por fetch se resuelve correctamente incluso, en aquellos casos en que el resultado es un 400, un 404, un 500 o similares.
Recuerda: la promise devuelta por fetch solo se resuelve con error si la petición no se puede realizar (p. ej. debido a un error de CORS o de red).
Así pues un código más realista podría ser:
fetch ('https://reqres.in/api/users',{mode: 'cors'}) .then(r => { if (r.ok) return r.json() return Promise.reject('Error code ' + r.status) }) .then(users => console.log(users.data[0].first_name)) .catch(err => console.error(err));
Si el valor devuelto por ok es false, indica que ha habido un error (p. ej. un 500) y en este caso usamos Promise.reject para devolver una promise rechazada. De este modo, mediante el único catch del final podemos capturar todos los errores; ya sean de la primera promise devuelta por fetch, o bien, de la promise rechazada que lanzamos nosotros.
Para ver cómo funciona en el caso de una petición que devuelva un código HTTP erróneo como 404, vamos a la URL “https://reqres.in/api/users/23” que devuelve un 404 y observamos cómo aparece un error en la consola con el mensaje “Error code 404”.
DEMO: Usando fetch (1)
Usaremos una API de pruebas como {JSON} Placeholder.
fetch ('https://jsonplaceholder.typicode.com/users')
Como es una promise, podemos usar then y tener un código que se ejecute una vez la promise haya finalizado:
fetch ('https://jsonplaceholder.typicode.com/users') .then(r => { console.log(r) });
Esto devuelve un objeto de tipo response. Tenemos que usar métodos de este objeto para poder obtener los datos.
DEMO: Usando fetch (2)
Como vimos antes, r es un objeto de tipo Response. Para leer los datos de la respuesta, podemos usar el método text, que devuelve otra promesa, así que finalmente para obtener el texto:
fetch ('https://jsonplaceholder.typicode.com/users') .then(r => r.text()).then(t => console.log(t));
Si sabemos que la respuesta viene en formato JSON, podríamos hacer:
fetch ('https://jsonplaceholder.typicode.com/users') .then(r => r.text()).then(t => console.log(JSON.parse(t)));
Podemos hacerlo de otra manera para simplificar utilizando el método json:
fetch ('https://jsonplaceholder.typicode.com/users') .then(r => r.json()).then(j => console.log(j));
DEMO: Usando fetch (3)
Obtengamos las cabeceras que nos envía el servidor y confirmar que la respuesta viene en formato JSON.
fetch ('https://jsonplaceholder.typicode.com/users').then(r => console.log(r));
El objeto Response tiene una propiedad llamada headers con un conjunto de funciones para acceder a estas cabeceras. Una de ellas es has que indica el tipo de datos de la respuesta.
Vamos a ver todas las cabeceras:
fetch ('https://jsonplaceholder.typicode.com/users').then(r => console.log(r.headers.keys()));
r.headers.keys() es un “iterable” que nos permite obtener todos los nombres de todas las cabeceras que manda el servidor.
Veámoslas como un array:
fetch ('https://jsonplaceholder.typicode.com/users').then(r => console.log([...r.headers.keys()]));
Queremos ver el valor de una cabecera en concreto:
fetch ('https://jsonplaceholder.typicode.com/users').then(r => console.log(r.headers.get("expires")));
Veamos ahora el tipo de la respuesta:
fetch ('https://jsonplaceholder.typicode.com/users').then(r => console.log(r.headers.get("content-type")));
Basándonos en esto podríamos crear una función que devolviese true si una petición devuelve JSON y la vamos a agregar al objeto Headers.
Headers.prototype.isJson = function() { return this.has("content-type") && this.get("content-type").substring(0, "application/json".length) == "application/json" }
Al ejecutar el código anterior, se habrá añadido el método isJson a Headers.Protytype
Ahora podríamos hacer la siguiente consulta:
fetch ('https://jsonplaceholder.typicode.com/users').then(r => console.log(r.headers.isJson()));
Y así sabemos si una petición devuelve JSON o no.







