Servir et interrompre un export HTTP
Objectif : Transmettre un export au client et libérer les ressources si la connexion est interrompue.
Un export peut être correctement généré côté serveur sans parvenir jusqu’au client : la connexion peut être lente ou coupée en cours de transfert. Si le serveur continue alors à lire la base et à produire des lignes que personne ne reçoit, il gaspille des ressources.
Vous allez relier les flux CSV déjà construits à la réponse HTTP, puis annuler le traitement lorsque le client ferme la connexion. La pagination par curseur, la lecture par lots et le contrôle de flux ont été traités dans les leçons précédentes ; ici, l’attention porte sur le cycle de vie de la réponse réseau.
Préparer la réponse de téléchargement
Pour déclencher un téléchargement, la route indique le type de contenu et propose un nom de fichier avec l’en-tête Content-Disposition. Le nom ci-dessous est fixe : si vous le construisez à partir d’une valeur fournie par le client, vous devrez la valider pour éviter qu’elle ne contienne des caractères de contrôle ou des séparateurs de chemin.
Le fichier src/routes/commandes.js ci-dessous expose une fabrique de route. Elle reçoit le service d’export et le journal au lieu de créer ces dépendances elle-même ; vous pouvez ainsi relier la route aux composants configurés par l’application.
const express = require('express');
const { pipeline } = require('node:stream/promises');
function creerRouteExport({ serviceExports, journal }) {
const routeur = express.Router();
routeur.get('/export.csv', async (requete, reponse) => {
const controleur = new AbortController();
const annulerSiDeconnecte = () => {
if (!reponse.writableFinished) {
controleur.abort();
}
};
reponse.once('close', annulerSiDeconnecte);
try {
reponse.status(200);
reponse.setHeader('Content-Type', 'text/csv; charset=utf-8');
reponse.setHeader(
'Content-Disposition',
'attachment; filename="commandes.csv"'
);
await pipeline(
serviceExports.creerFluxCommandes(),
serviceExports.creerFluxCsv(),
reponse,
{ signal: controleur.signal }
);
journal.info({ evenement: 'export_termine' });
} catch (erreur) {
if (erreur.name === 'AbortError') {
journal.info({ evenement: 'export_interrompu' });
} else {
journal.error({
evenement: 'echec_export',
message: erreur.message
});
if (!reponse.headersSent && !reponse.destroyed) {
reponse.status(500).json({ erreur: 'Export indisponible' });
}
}
} finally {
reponse.removeListener('close', annulerSiDeconnecte);
}
});
return routeur;
}
module.exports = { creerRouteExport };Le routeur peut être monté par l’application sous le chemin de votre choix ; la route elle-même répond ici sur /export.csv. La fabrique attend un service qui fournit les méthodes creerFluxCommandes() et creerFluxCsv(), conformément au service d’export établi dans les leçons précédentes.
Interrompre le travail si le client part
Le pipeline transmet les données des flux à la réponse et propage les erreurs entre ses étapes. En lui passant le signal d’un AbortController, vous rendez possible son annulation : lorsqu’elle survient, les flux reliés au pipeline sont détruits et la lecture en amont peut s’arrêter au lieu de continuer inutilement.
La route écoute l’événement close de la réponse. Elle vérifie writableFinished avant d’annuler : cet indicateur permet de ne pas traiter une réponse terminée normalement comme une déconnexion prématurée. À l’inverse, n’utilisez pas ici l’événement close de la requête entrante : celle-ci peut avoir fini d’être lue alors que l’export est toujours en cours d’envoi.
Distinguer interruption et échec
Une annulation déclenche une erreur portant le nom AbortError. Le code la journalise comme une interruption attendue, tandis qu’une erreur de lecture ou de transformation est enregistrée comme un échec. Cette distinction aide l’exploitation : une déconnexion du client ne signifie pas nécessairement que la base ou le service CSV est défaillant.
La réponse JSON de secours n’est envoyée que si les en-têtes n’ont pas encore été transmis et que la réponse n’est pas détruite. Après le début du téléchargement, le statut HTTP ne peut plus être changé de façon fiable ; la journalisation conserve alors l’information utile sans tenter d’écrire une seconde réponse sur la connexion.
Enfin, le bloc finally retire l’écouteur ajouté pour cette requête, qu’elle ait réussi, échoué ou été annulée. Ce nettoyage évite de conserver des références à la réponse après la fin du traitement. Le service de commandes et ses classes d’interface ne changent pas : cette leçon ajoute un comportement côté serveur uniquement.
À retenir
- Définissez les en-têtes de téléchargement avant de démarrer l’envoi du flux.
- Reliez les flux d’export à la réponse avec `pipeline` pour propager les erreurs.
- Annulez le pipeline quand la réponse est fermée avant sa fin normale.
- Distinguez une annulation d’une erreur de traitement et retirez toujours l’écouteur de fermeture.
Dans la prochaine leçon, vous choisirez une stratégie d’authentification adaptée aux clients du service.