JS. Manejando respuestas parciales de fetch

Diferencias entre XMLHttpRequest y fetch para manejar las respuestas parciales de un request

Generalmente cuando hacemos un request desde el browser esperamos 1 respuesta:

const response = await fetch('/'),
    text = await response.text();

// ta-da!

Pero qué pasa si esa respuesta es lenta y en el medio queremos ir obteniendo resultados parciales? Cómo los podemos ir mostrando?

Escenario

Vamos a poner como ejemplo que desde el FE (Front End), queremos ejecutar un shell script en nuestro servidor e ir obteniendo el output que da, digamos que el script a ejecutar es algo así:

test.sh

#!/bin/sh

echo Uno
sleep 2
echo Dos
sleep 2
echo Tres
sleep 2
echo Fin!

En total la ejecución de ese script será de 6 segundos, y nosotros queremos ir mostrando cada mensaje a medida que los va sacando ese script.

Usando XMLHttpRequest

El primer approach que se me ocurrió fue usar XMLHttpRequest.

<form onsubmit="enviarComando(event)">
    <input type="text"></input>
</form>

<pre class="resultado"></pre>

<script>
    const contenedorResultado = document.querySelector('.resultado');

    function enviarComando(e) {
        e.preventDefault();

        const input = e.target.children[0],
            comando = input.value;

        input.value = '';
        contenedorResultado.textContent = '';

        const request = new XMLHttpRequest();

        request.onprogress = (progreso) => {
            contenedorResultado.textContent = progreso.target.responseText;
        };

        request.open('POST', '/');
        request.send(comando);
    }
</script>

Para ver el código completo con el servidor y script pueden ver este repositorio.

En Firefox funciona genial, pero en Chrome, ay Chromcito…

¿Por qué no funciona en Chrome? No sé. Pero hay otra alternativa…

Axios

Esta no. Usa XMLHttpRequest por debajo y entonces el evento onDownloadProgress tiene el mismo problema en Chrome.

Fetch

Siempre usé la funcionalidad básica del fetch, transformar toda una respuesta a texto o json y siga.

Pero hay una funcionalidad, relativamente nueva, con algunos features en desarrollo, pero con Firefox 65+ y Chrome 42+ se puede usar. Pueden leer más detalles en este artículo de MDN.

Para este ejemplo, quedaría así:

<form onsubmit="enviarComando(event)">
    <input type="text"></input>
</form>

<pre class="resultado"></pre>

<script>
    const contenedorResultado = document.querySelector('.resultado');

    async function enviarComando(e) {
        e.preventDefault();

        const input = e.target.children[0],
            comando = input.value;

        input.value = '';
        contenedorResultado.textContent = '';

        const respuesta = await fetch('/', {
            method: 'POST',
            body: comando
        });

        const reader = respuesta.body.getReader();

        let fin = false;

        while(!fin) {
            const lectura = await reader.read(),
                texto = new TextDecoder('utf-8').decode(lectura.value);

            fin = lectura.done;
            contenedorResultado.textContent += texto;
        }
    }
</script>

Aunque un poco más complicado, porque hay que transormar un Uint8Array a string, ahora funciona tanto en Chrome como en Firefox!

Y probablemente a futuro podamos concatenar streams para el procesamiento de los resultados que vayan llegando.

Websockets

Es cierto que esto mismo es posible con Websockets, aunque la implementación aumenta un poco en complejidad, gran parte de la complejidad siendo el manteniemiento de estas conexiones.

Obvio que tiene su caso de uso, y cada uno decidirá en dónde es mejor usarlo, pero ahora saben de otra alternativa.

Conclusiones

Aunque me da un poco de bronca que una forma tan vieja de hacer requests como XMLHttpRequest no funcione en Chrome, me gusta lo que se viene.

Espero que al haber visto esto hayan podido expandir las posibilidades que se tiene desarrollando en la web y lo que se viene.