Envoyer des fichiers avec Uppy, XHRUpload et Express
Utilisez le Dashboard d’Uppy pour sélectionner des fichiers et XHRUpload pour les envoyer à un point de terminaison multipart. Ce tutoriel vous fournit un exemple local complet, du navigateur au serveur : un sélecteur de fichiers avec progression et contrôles de nouvelle tentative, ainsi qu’un récepteur Express qui renvoie le nombre d’octets et l’empreinte SHA-256 de chaque fichier envoyé.
Choisir le contrat d’envoi
Le Dashboard fournit l’interface ; XHRUpload envoie une requête
POST multipart par fichier. Le récepteur attend le champ file et répond en JSON après avoir lu
le fichier entier. Il n’y a pas de second XMLHttpRequest écrit à la main à côté d’Uppy.
Il s’agit d’une démonstration de transfert en local. Le récepteur conserve chaque fichier en mémoire pendant le traitement de sa requête, en calcule l’empreinte, puis le supprime. Il n’enregistre pas les fichiers, ne renvoie pas de liens de téléchargement et n’inspecte pas leur contenu. Une réponse réussie signifie que le récepteur a lu les octets, pas qu’il les a stockés durablement.
Créer le projet
Utilisez Bash sous Linux, Node.js 24.15.0 et Yarn 4.12.0 disponible via corepack yarn. L’exemple
fonctionne aussi sous Node.js 26.8.1 ; la vérification dans le navigateur utilise Chromium 145.
Node exécute server.ts grâce à la
suppression native des types TypeScript.
Le code destiné au navigateur passe par esbuild.
Collez ceci dans un répertoire où vous souhaitez créer un nouveau dossier uppy-xhr-demo. Les
parenthèses maintiennent votre shell dans son répertoire d’origine. Si ce dossier existe déjà, la
mise en place s’arrête sans le modifier ; choisissez un nouvel emplacement. Si l’installation
échoue, arrêtez-vous et résolvez le problème avant de continuer.
(
mkdir uppy-xhr-demo &&
cd uppy-xhr-demo &&
printf '%s\n' '{"private":true,"type":"module","packageManager":"yarn@4.12.0"}' > package.json &&
printf '%s\n' 'nodeLinker: node-modules' > .yarnrc.yml &&
touch yarn.lock &&
corepack yarn add --exact @uppy/core@6.0.2 @uppy/dashboard@6.0.0 @uppy/xhr-upload@6.0.0 express@5.2.1 multer@2.4.0 esbuild@0.27.0 &&
mkdir public
)
Le fichier de verrouillage vide fait de ce dossier un projet Yarn distinct, y compris à l’intérieur
d’un autre projet. Conservez le yarn.lock obtenu pour des installations reproductibles de la
combinaison testée. Le package.json local impose les modules ES, même dans un projet parent
CommonJS ; le linker node-modules de Yarn permet à Node, sans outil
supplémentaire, de résoudre les imports du serveur.
Enregistrez les trois fichiers suivants dans uppy-xhr-demo.
Ajouter le récepteur
Enregistrez ceci sous server.ts. Multer analyse le corps
multipart et applique les limites de requête pendant sa réception. Le récepteur accepte un fichier
par requête, jusqu’à 2 MiB, sans champ texte supplémentaire. La vérification MIME ne contrôle que la
déclaration de l’expéditeur ; elle ne prouve pas que les octets constituent une image ou un PDF
valide.
import { createHash } from 'node:crypto'
import { join } from 'node:path'
import express from 'express'
import multer from 'multer'
const app = express()
const receive = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 2 * 1024 * 1024, files: 1, fields: 0 },
}).single('file')
const allowedTypes = new Set(['image/jpeg', 'image/png', 'application/pdf'])
app.use(express.static(join(import.meta.dirname, 'public')))
app.post('/upload', (req, res) => {
receive(req, res, (error: unknown) => {
if (error) {
const tooLarge = error instanceof multer.MulterError && error.code === 'LIMIT_FILE_SIZE'
res.status(tooLarge ? 413 : 400).json({ error: 'Upload rejected.' })
return
}
const file = req.file
if (!file) {
res.status(400).json({ error: 'Expected one file in the file field.' })
return
}
if (!allowedTypes.has(file.mimetype)) {
res.status(415).json({ error: 'Expected a JPEG, PNG, or PDF MIME type.' })
return
}
res.json({
bytes: file.size,
sha256: createHash('sha256').update(file.buffer).digest('hex'),
})
})
})
const port = Number(process.env.PORT ?? 0)
if (!Number.isInteger(port) || port < 0 || port > 65535) {
throw new Error('PORT must be an integer from 0 to 65535.')
}
const server = app.listen(port, '127.0.0.1', (error?: Error) => {
if (error) {
console.error(`Cannot start the upload server: ${error.message}`)
process.exitCode = 1
return
}
const address = server.address()
if (!address || typeof address === 'string') throw new Error('Missing listening address.')
console.log(`Open http://127.0.0.1:${address.port}`)
})
Le port zéro demande au système d’exploitation un port disponible. Vous pouvez définir PORT sur
un port fixe si nécessaire. Le callback gère les
erreurs de démarrage d’Express 5, de sorte qu’un port occupé
entraîne une sortie en échec au lieu d’afficher une adresse trompeuse indiquant que le serveur est
prêt.
Ajouter la page et le module d’envoi
Enregistrez ceci sous public/index.html :
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Uppy upload demo</title>
<link rel="icon" href="data:," />
<link rel="stylesheet" href="/app.css" />
<style>
body { margin: 1rem; font-family: sans-serif; }
#receipts { overflow-wrap: anywhere; }
</style>
<script type="module" src="/app.js"></script>
</head>
<body>
<h1>Upload to the local receiver</h1>
<div id="drag-drop-area"></div>
<h2>Received by the server</h2>
<p>These receipts confirm transfer. Files are not saved.</p>
<ul id="receipts" aria-label="Server receipts" aria-live="polite"></ul>
</body>
</html>
Enregistrez ceci sous client.ts. Le Dashboard affiche déjà
les erreurs de sélection, la progression de l’envoi, l’annulation et les contrôles de nouvelle
tentative. Le petit gestionnaire d’événements ajoute les reçus du serveur à l’aide de
textContent, de sorte qu’un nom de fichier est affiché en tant que texte.
import Uppy from '@uppy/core'
import Dashboard from '@uppy/dashboard'
import XHRUpload from '@uppy/xhr-upload'
import '@uppy/core/css/style.min.css'
import '@uppy/dashboard/css/style.min.css'
const receipts = document.querySelector('#receipts')
if (!(receipts instanceof HTMLUListElement)) throw new Error('Missing receipt list.')
const uppy = new Uppy({
autoProceed: false,
allowMultipleUploadBatches: false,
restrictions: {
maxFileSize: 2 * 1024 * 1024,
maxNumberOfFiles: 5,
allowedFileTypes: ['image/jpeg', 'image/png', '.pdf'],
},
})
.use(Dashboard, {
inline: true,
target: '#drag-drop-area',
note: 'Up to five JPEG, PNG, or PDF files, each up to 2 MiB.',
})
.use(XHRUpload, {
endpoint: '/upload',
fieldName: 'file',
formData: true,
bundle: false,
allowedMetaFields: false,
limit: 2,
shouldRetry: () => false,
onAfterResponse(xhr) {
if (xhr.status === 413) throw new Error('Choose a file no larger than 2 MiB.')
if (xhr.status === 415) throw new Error('The server requires a JPEG, PNG, or PDF MIME type.')
if (xhr.status < 200 || xhr.status >= 300) {
throw new Error('The server rejected the upload. Check the endpoint before retrying.')
}
},
})
uppy.on('upload-success', (file, response) => {
if (!file) return
const item = document.createElement('li')
item.textContent = `${file.name}: ${JSON.stringify(response.body)}`
receipts.append(item)
})
fieldName correspond à single('file') côté récepteur. allowedMetaFields: false omet les champs de
métadonnées par défaut d’Uppy afin de respecter la limite de zéro champ du serveur. Ne définissez
pas vous-même d’en-tête Content-Type : le navigateur fournit la délimitation multipart. Deux fichiers
peuvent être transférés simultanément ; les fichiers restants attendent dans la file d’Uppy.
Compiler et tester un envoi
Depuis le même répertoire parent que celui où vous avez lancé la mise en place, compilez le bundle du navigateur et démarrez le serveur :
(
cd uppy-xhr-demo &&
corepack yarn exec esbuild client.ts --bundle --format=esm --outfile=public/app.js &&
node server.ts
)
esbuild génère à la fois public/app.js et public/app.css.
Le CSS nécessite son propre lien dans le HTML,
ce que la page ci-dessus inclut. Une nouvelle compilation remplace ces deux fichiers générés.
Arrêtez le serveur au premier plan avec Ctrl+C avant de recompiler, puis rechargez la page après
avoir redémarré le serveur.
Ouvrez l’adresse exacte affichée par le serveur. Choisissez des fichiers avec
browse files, puis utilisez le bouton d’envoi du Dashboard. Pour un
seul fichier sélectionné, il affiche Upload 1 file. Chaque fichier
accepté ajoute un reçu sous Received by the server avec bytes et
sha256. Un fichier vide portant un nom de PDF est une entrée valide pour cette démonstration de
transfert et reçoit bytes: 0 ; cela n’en fait pas un document PDF valide.
L’exemple traite un lot de sélection à la fois. Une fois l’envoi commencé, les nouvelles sélections sont bloquées. Relancez les fichiers en échec de ce lot, ou rechargez la page pour un nouveau lot une fois l’envoi terminé. L’annulation vide la sélection afin que vous puissiez choisir à nouveau. Les reçus déjà affichés restent visibles jusqu’au rechargement de la page.
Une barre de progression atteignant 100 % décrit le transfert, pas l’acceptation par le serveur.
Seul upload-success ajoute un reçu. Dans votre propre intégration, ne considérez pas
l’événement complete d’Uppy comme la preuve que tous les fichiers
ont réussi : il se déclenche aussi lorsque des fichiers ont échoué, et fournit des tableaux
successful et failed distincts.
Garder la validation côté serveur
Les restrictions côté client permettent de détecter plus facilement les erreurs avant l’envoi des données. Un autre client peut les contourner. Ce récepteur limite séparément la taille des requêtes et le nombre de fichiers, mais sa vérification MIME fait toujours confiance aux métadonnées fournies par l’appelant. Il n’effectue jamais de rendu des octets envoyés, ne les exécute jamais et ne les sert jamais en retour.
Avant de l’adapter à un service public, ajoutez l’authentification et l’autorisation, une validation du contenu pour les formats que vous acceptez et un stockage adapté à votre application. Appliquez-y aussi des limites de requêtes et de concurrence : de petites limites par fichier ne plafonnent pas la mémoire totale utilisée par de nombreux clients simultanés. La démo n’écoute que sur l’adresse de loopback locale et ne dispose ni d’authentification ni de stockage persistant.
Résoudre les problèmes courants
Configuration CORS
Servez la page depuis l’adresse HTTP affichée, au lieu d’ouvrir index.html comme fichier local. La
page et /upload partagent la même origine ; cet exemple n’a donc besoin d’aucun middleware CORS.
Si vous déplacez l’API vers une autre origine, configurez ce serveur pour autoriser votre véritable
origine frontend et les en-têtes requis.
CORS contrôle l’accès du navigateur aux réponses ;
il n’authentifie pas une requête d’envoi.
Erreurs réseau
Cet exemple désactive les nouvelles tentatives automatiques avec shouldRetry: () => false afin de rendre chaque
tentative observable. Arrêtez le serveur après avoir chargé la page, puis lancez un envoi pour voir
l’état d’échec du Dashboard. Redémarrez le serveur sur le même port, ou rechargez la page à sa
nouvelle adresse et sélectionnez à nouveau les fichiers.
En cas d’échec d’une requête sur le même point de terminaison, corrigez la cause et choisissez Retry. XHRUpload renvoie alors ce fichier depuis le début. Une réponse perdue peut laisser le navigateur dans l’incertitude, même si le serveur a reçu les octets. Un service d’envoi persistant a besoin de sa propre politique de gestion des doublons.
Utilisez la limitation réseau du navigateur avec un fichier autorisé plus volumineux pour tester le contrôle Cancel de la barre d’état pendant qu’un travail est en cours. L’annulation interrompt les requêtes en attente du navigateur et vide la sélection ; elle ne peut pas retirer les octets que le serveur a déjà reçus. Il ne s’agit pas d’une pause/reprise.
Validation du type de fichier
Un fichier dépassant la limite côté client, une extension ou un type non pris en charge, ou un
sixième fichier produit une erreur de sélection du Dashboard sans lancer l’envoi de ce fichier. La
règle .pdf autorise l’extension du nom de fichier ; elle n’inspecte pas le contenu du PDF.
HTTP 415 signifie que le récepteur a rejeté le type MIME déclaré. HTTP 413 signifie que sa limite de
taille a été dépassée. Inspectez la requête POST dans le panneau réseau du navigateur ; modifier les
paramètres CORS ne corrigera pas ces réponses.
Utiliser tus lorsque les envois doivent pouvoir reprendre
XHRUpload convient bien aux points de terminaison multipart classiques et aux petits fichiers. Modifier sa limite de concurrence n’ajoute ni découpage en morceaux ni reprise. Pour les envois qui doivent se poursuivre après une interruption, utilisez le plugin Tus d’Uppy avec un serveur compatible tus. Il s’agit d’un protocole différent, qui nécessite de remplacer ce récepteur multipart.
