Progression d'une requête http

Dans le cadre du développement de mon application de lecture/gestion de comic books, il a été nécessaire de gérer le temps de chargement des archives .cbz, qui peut durer plusieurs secondes ; j'ai dû afficher une barre de progression pendant le temps que dure la requête au serveur. Quelques notes sur le sujet.

Les exemples donnés sont simplifiés afin de présenter les fonctionnalités.

Une requête qui prend du temps

Les archives bédés (.cbz) peuvent avoir une taille de plusieurs dizaines de Mo. Sur un réseau local moyen comme le mien, où la communication entre raspi et pc se fait à raison de 2 Mo/s, le chargement d'une bédé peut prendre jusqu'à une dizaine de secondes.

Dans ce type de situation, on peut faire patienter l'utilisateur à l'aide d'une jauge de progression (un élément progress par exemple). Encore faut-il pouvoir faire progresser cette jauge durant le temps que dure la requête http.

Web : progression d'une requête HTTP

Utilisation de Fetch

l'API fetch permet de « mesurer » la progression d'une requête http à l'aide de la propriété body de la réponse, un stream lisible qui donne donc à lire les parties de la réponse http au fur et à mesure qu'elles arrivent.

Voici une manière parmi d'autre de l'utiliser :

// http Response
const response = await globalThis.fetch(url);
// get reader of the ReadableStream
const reader = response.body.getReader();
// data storage
const chunks = [];

// read data
let done, value;
while(!done) {
    ({done, value} = await reader.read());
    if (done) {
        break;
    }
    chunks.push(value);
}

// Do something with data
console.log(chunks);

Pour faire progresser une jauge durant la requête, on peut se baser (1) sur la taille total de la réponse (l'en-tête "Content-Length") et les tailles de chaque morceau de données successives :

// http Response
const response = await globalThis.fetch(url);
// get reader of the ReadableStream
const reader = response.body.getReader();
// data storage
const chunks = [];
// total size
const total = Number(response.headers.get('Content-Length') || 1);
// Loaded size & progress
let loaded = 0; 

// read data
let done, value;
while(!done) {
    ({done, value} = await reader.read());
    if (done) {
        break;
    }
    chunks.push(value);
    
    // calculate progress
    loaded += value.length;
    console.log(`progress: ${(loaded/total) * 100}%`);
}

// Do something with data
console.log(chunks);

Le calcul peut être fausser si le serveur n'envoie pas l'en-tête "Content-Length" ou si les échanges http sont compressés ("Content-Encoding").

On utilise ensuite le tableau chunks pour ce qu'on a besoin de faire, un Blob par exemple :

const blob = new Blob(chunks);

Utilisation de XmlHttpRequest

On peut toujours effectuer la requête avec l'objet historique XmlHttpRequest qui possède un événement "progress" dédié :

const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onprogress = e => {
    console.log(`Progress: ${(e.loaded/e.total) * 100}%`);
};
xhr.onload = e => {
    console.log('loaded');
    // Do something with data
};
xhr.onerror = e => {
    console.log('Error');
};
xhr.send();

Serveur : générer un stream sous node.js

Néanmoins, pour que cela fonctionne, il faut évidemment que le serveur http renvoie la réponse sous forme de stream. Voici par exemple une manière de faire dans node.js :

import http from 'http';
import fs from 'fs';

/**
 * @param {http.ServerResponse} res 
 * @param {string} filepath 
 */
const streamResponse = (res, filepath) => {
    res.setHeader('Content-Length', `${fs.statSync(filepath).size}`);
    const stream = fs.createReadStream(filepath);
    stream.on('open', () => {
        stream.pipe(res);
    });
    stream.on('error', err => {
        res.statusCode = 500;
        res.write(err.message);
        res.end();
    });
    stream.on('end', () => {
        res.end();
    });
};

Ressources et références