ITSkillsCenter
Développement Web

Uploads directs avec URLs pré-signées sur Hetzner Storage : tutoriel 2026

12 دقائق للقراءة

📍 Article principal : Hetzner Object Storage en production 2026

Introduction

Une plateforme de gestion documentaire à Abidjan recevait depuis ses bureaux d’Adjamé environ 800 uploads de PDF et photos par jour, chaque fichier mesurant entre 2 et 50 Mo. L’architecture initiale faisait transiter chaque fichier par le serveur applicatif Node : le client uploadait vers le serveur, qui re-uploadait vers AWS S3. Ce double transit saturait régulièrement la bande passante Hetzner CX22 et provoquait des timeouts pendant les heures de pointe. La migration vers le pattern d’upload direct via URL pré-signée — le client envoie directement vers le bucket Hetzner sans passer par le serveur — a éliminé le goulot et réduit la charge serveur de 70 %. Ce tutoriel décrit l’implémentation complète : génération côté serveur, upload côté client avec progression, validation, gestion des erreurs réseau, et patterns multipart pour les gros fichiers. À la fin, vous avez un système d’upload qui scale automatiquement et qui reste fluide même sur connexion 4G ouest-africaine instable.

Prérequis

  • Bucket Hetzner Object Storage configuré (voir le pilier)
  • Backend Node, Bun ou Deno avec AWS SDK v3 installé
  • Frontend qui consomme votre API (SvelteKit, React, Vue, vanilla JS)
  • Niveau : intermédiaire — Temps : 1 h 30

Étape 1 — Principe et architecture

L’upload direct via URL pré-signée fonctionne en deux temps. D’abord, le client demande au serveur applicatif de générer une URL signée pour le fichier qu’il veut uploader. Le serveur authentifie la requête, vérifie les autorisations, et signe une URL S3 valable quelques minutes avec les permissions précises (PUT sur cette clé spécifique). Ensuite, le client uploade le fichier directement vers Hetzner via cette URL signée, sans plus passer par le serveur. Le bucket Hetzner accepte la requête grâce à la signature temporairement valide.

Cette architecture présente quatre avantages mesurables. Premièrement, la bande passante du serveur applicatif n’est plus consommée par les transferts de fichiers — elle reste disponible pour les requêtes API rapides. Deuxièmement, le serveur peut tourner sur une instance plus modeste : un VPS Hetzner CX22 suffit même pour des centaines d’uploads par minute. Troisièmement, l’upload est plus rapide pour le client puisqu’il n’y a qu’un seul transit au lieu de deux. Quatrièmement, le serveur ne stocke jamais les fichiers temporairement, éliminant tout un pan de gestion d’erreurs (espace disque plein, fichiers orphelins).

Étape 2 — Générer l’URL pré-signée côté serveur

Avec l’AWS SDK v3 et le helper @aws-sdk/s3-request-presigner, la génération tient en quelques lignes. On crée une commande PutObject avec les paramètres souhaités, on la passe au signer, et on obtient l’URL valable un certain temps. Pour la sécurité, on ajoute systématiquement des contraintes : type de contenu autorisé, taille maximale, et expiration courte.

import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const s3 = new S3Client({
  region: 'fsn1',
  endpoint: 'https://fsn1.your-objectstorage.com',
  credentials: { accessKeyId: ACCESS_KEY, secretAccessKey: SECRET }
});

app.post('/api/uploads/sign', requireAuth, async (c) => {
  const userId = c.get('userId');
  const { nomFichier, type, taille } = await c.req.json();

  if (!['image/jpeg','image/png','application/pdf'].includes(type))
    return c.json({ erreur: 'Type non autorisé' }, 400);
  if (taille > 10_000_000)
    return c.json({ erreur: 'Fichier > 10 Mo' }, 400);

  const cle = `uploads/${userId}/${crypto.randomUUID()}-${nomFichier}`;
  const url = await getSignedUrl(
    s3,
    new PutObjectCommand({
      Bucket: 'itskills-uploads',
      Key: cle,
      ContentType: type,
      ContentLength: taille
    }),
    { expiresIn: 600 }
  );

  return c.json({ uploadUrl: url, cle, urlPubliquePost: `https://medias.example.sn/${cle}` });
});

Trois précautions visibles dans ce code. Premièrement, l’authentification via requireAuth garantit que seuls les utilisateurs connectés obtiennent une URL. Deuxièmement, la validation du type MIME et de la taille empêche les uploads de fichiers non autorisés ou trop gros — la contrainte est imposée serveur, pas seulement côté client. Troisièmement, la clé du fichier inclut l’ID utilisateur, ce qui permet de tracer qui a uploadé quoi et de configurer des policies S3 par utilisateur si nécessaire.

Étape 3 — Uploader côté client

Côté navigateur, l’upload via URL pré-signée est un PUT HTTP standard. On peut utiliser fetch natif pour la simplicité, ou XMLHttpRequest si on veut accéder à l’événement de progression. Pour les uploads typiques sous 100 Mo, fetch suffit largement et bénéficie de la simplicité d’API.

async function uploaderFichier(file: File): Promise<{ url: string }> {
  // Étape 1 : demander l'URL signée au serveur
  const sign = await fetch('/api/uploads/sign', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ nomFichier: file.name, type: file.type, taille: file.size })
  });
  const { uploadUrl, urlPubliquePost } = await sign.json();

  // Étape 2 : uploader directement vers Hetzner
  const upload = await fetch(uploadUrl, {
    method: 'PUT',
    headers: { 'Content-Type': file.type },
    body: file
  });
  if (!upload.ok) throw new Error('Upload échoué : ' + upload.status);

  return { url: urlPubliquePost };
}

Pour afficher la progression en temps réel, on remplace fetch par XMLHttpRequest qui expose upload.onprogress. Cette amélioration UX vaut vraiment l’investissement pour les utilisateurs ouest-africains avec connexion lente — voir un fichier de 30 Mo monter de 0 % à 100 % rassure et évite les abandons. Pour les composants Svelte ou React, plusieurs librairies (react-dropzone, svelte-dropzone) encapsulent ce pattern avec drag-and-drop.

Étape 4 — Multipart pour gros fichiers

Pour les fichiers de plus de 100 Mo (vidéos, archives, datasets), l’upload monolithique devient fragile : si la connexion coupe à 80 %, on doit tout recommencer depuis zéro. Le multipart upload résout cela en découpant le fichier en chunks (typiquement 5 à 10 Mo), chacun uploadé indépendamment et reprenable en cas d’échec. L’AWS SDK le gère automatiquement via @aws-sdk/lib-storage.

Pour Hetzner Object Storage, le multipart fonctionne identiquement à AWS S3. Le serveur génère plusieurs URLs pré-signées (une par part) ou utilise des jetons de session multipart. Le client uploade les parts en parallèle et finalise l’upload quand tous les chunks sont reçus. Pour les uploads de vidéos par les coursiers d’une plateforme logistique à Cotonou par exemple, ce pattern fait passer le taux de réussite de 60 % (uploads de 200 Mo monolithiques) à 99 % avec retry automatique des chunks échoués.

Étape 5 — Configurer CORS sur le bucket

Sans configuration CORS, le navigateur refuse les uploads cross-origin (votre frontend example.sn vers Hetzner fsn1.your-objectstorage.com). On configure les règles CORS du bucket via la console Hetzner ou via une commande AWS SDK. Configuration typique : autoriser les méthodes PUT, GET, POST, OPTIONS depuis vos domaines de production et de développement, exposer les headers nécessaires.

// Configuration CORS appliquée une fois
import { PutBucketCorsCommand } from '@aws-sdk/client-s3';

await s3.send(new PutBucketCorsCommand({
  Bucket: 'itskills-uploads',
  CORSConfiguration: {
    CORSRules: [{
      AllowedOrigins: ['https://example.sn', 'http://localhost:5173'],
      AllowedMethods: ['PUT','GET','POST','HEAD'],
      AllowedHeaders: ['*'],
      ExposeHeaders: ['ETag'],
      MaxAgeSeconds: 3600
    }]
  }
}));

Sans cette configuration, on observe une erreur « CORS policy » dans la console navigateur sans détail spécifique. C’est l’un des pièges les plus courants des premiers tests d’upload direct — toujours vérifier les règles CORS après création du bucket avant de tester l’upload depuis le frontend.

Étape 6 — Sécurité et abus

Trois protections additionnelles à activer en production. Premièrement, le rate limiting sur l’endpoint /api/uploads/sign : un utilisateur ne devrait pas pouvoir générer 1 000 URLs signées par minute. Implémenter une limite de 10 URLs par utilisateur par minute via Redis ou KV évite les abus. Deuxièmement, la validation post-upload côté serveur : un endpoint /api/uploads/finalize reçoit la confirmation du client après upload, vérifie via S3 HEAD que le fichier existe et a la taille déclarée, et seulement alors enregistre la référence en base de données. Cette double vérification empêche les utilisateurs de prétendre avoir uploadé alors qu’ils n’ont rien fait. Troisièmement, le scan antivirus pour les fichiers sensibles via ClamAV ou un service externe — délégué à une queue asynchrone pour ne pas bloquer l’expérience utilisateur.

Pour les SaaS qui acceptent des fichiers très sensibles (documents médicaux, justificatifs financiers, contrats légaux), le chiffrement côté client avant upload ajoute une couche de défense supplémentaire. La clé de chiffrement est dérivée du mot de passe utilisateur ou d’une clé maître stockée séparément. Hetzner ne voit que des données chiffrées, et même une compromission complète du bucket ne révèle pas le contenu réel des fichiers. Cette protection est rare mais mérite considération pour les workloads à très haut enjeu.

Erreurs fréquentes

Erreur Cause Solution
« CORS policy blocked » CORS bucket non configuré Appliquer PutBucketCors avec origines autorisées
403 Forbidden au PUT URL signée expirée ou ContentType différent Vérifier expiresIn et headers exact
Upload lent depuis Dakar Pas de multipart pour gros fichier Implémenter multipart pour fichiers > 50 Mo
Fichiers orphelins en bucket Pas de cleanup des uploads abandonnés Cron quotidien qui supprime les uploads sans entrée DB
SignatureDoesNotMatch Region ou endpoint mauvais Vérifier fsn1 ou nbg1 exactement

Adaptation au contexte ouest-africain

Trois aspects pratiques. Premièrement, sur les connexions ouest-africaines variables, le multipart upload n’est plus une optimisation luxueuse mais une nécessité pour tout fichier de plus de 50 Mo. Sans multipart, les uploads échouent fréquemment et frustrent les utilisateurs. Deuxièmement, l’affichage de la progression et le retry automatique en cas d’échec partiel transforment l’expérience perçue : un utilisateur à Bobo-Dioulasso accepte mieux un upload de 5 minutes avec progression visible qu’un upload de 30 secondes qui échoue silencieusement. Troisièmement, pour les SaaS qui ciblent les zones rurales ou périphériques (Touba, Korhogo, Niamey rural), prévoir un mode de synchronisation différée : le fichier est mis en file d’attente dans IndexedDB côté navigateur, et l’upload se déclenche automatiquement dès que la connectivité revient. Cette robustesse est ce qui distingue une application « qui marche partout » d’une application qui ne fonctionne qu’avec une connexion premium.

Pour le coût total de l’architecture upload direct, on observe typiquement une réduction de 60 à 80 % sur la facture serveur applicatif (CPU, RAM, bande passante) couplée à une stabilité accrue. Pour une plateforme qui traite plusieurs milliers d’uploads quotidiens, l’économie représente facilement 30 à 100 euros par mois sans dégradation de l’expérience utilisateur.

Tutoriels frères

Pour aller plus loin

FAQ

Combien de temps doit durer une URL pré-signée ?
5 à 15 minutes selon le contexte. Pour un upload immédiat, 5 minutes suffisent. Pour un workflow où l’utilisateur peut prendre du temps à choisir son fichier, 15 minutes laissent de la marge.

Peut-on limiter la taille du fichier dans l’URL signée ?
Partiellement via ContentLength. Pour une vraie protection, valider côté serveur après upload via une HEAD request qui vérifie la taille réelle du fichier stocké.

Comment gérer les uploads simultanés multiples ?
Générer plusieurs URLs signées en parallèle côté serveur (un seul appel batch). Côté client, uploader en parallèle avec une limite de concurrence (3-5 simultanés pour éviter de saturer la bande passante).

Les URLs signées sont-elles plus sûres que les credentials directs ?
Oui, considérablement. Une URL signée a une portée limitée (un fichier précis, une opération précise, durée courte). Si elle fuite, l’attaquant peut au pire uploader un seul fichier dans un seul chemin pendant 10 minutes.

Patterns avancés et cas réels

Trois patterns avancés émergent en production sur des applications mature. Le premier est l’upload chunké côté client avec reprise locale : le fichier est découpé en chunks de 5 Mo, chaque chunk est tracé dans IndexedDB après upload réussi, et en cas de crash navigateur ou rafraîchissement de page, l’utilisateur reprend là où il s’était arrêté. Cette robustesse est précieuse pour les uploads longs sur connexion instable. Le second pattern est la génération en lot d’URLs pré-signées pour les workflows qui uploadent plusieurs fichiers simultanément (par exemple un photographe qui uploade 50 photos d’un événement en une fois). Un seul appel API génère 50 URLs, le frontend uploade en parallèle avec une concurrence limitée à 5, et la session reste fluide même si chaque photo fait 8 Mo. Le troisième pattern est la transformation post-upload via worker : une fois l’image uploadée, un job background la redimensionne en plusieurs résolutions (thumbnail, medium, large) stockées dans des chemins parallèles. L’utilisateur n’attend pas cette transformation, elle se fait en arrière-plan et le frontend pre-fetch les nouvelles versions quand elles sont prêtes.

Pour une plateforme e-commerce ouest-africaine qui accepte les photos de produits depuis les vendeurs, ces trois patterns combinés donnent une expérience comparable à Amazon ou Jumia avec une infrastructure très modeste. Le coût mensuel total d’un tel système (Hetzner Object Storage + serveur Hetzner CX22 pour les workers + Cloudflare frontal) tient sous 25 euros même pour des dizaines de milliers d’uploads par jour.

Monitoring des uploads

Pour superviser le système d’upload en production, deux niveaux de métriques. Au niveau infrastructure, surveiller le volume d’objets dans le bucket et leur croissance — une augmentation soudaine peut signaler un abus ou un bug dans la logique de cleanup. Au niveau applicatif, logger chaque demande d’URL signée et chaque finalisation réussie, calculer le taux de complétion (uploads finalisés / URLs signées) qui devrait dépasser 90 % en conditions normales. Une chute de ce taux signale un problème : URL CORS cassée, validation trop stricte, ou souci réseau côté utilisateurs.

Pour les SaaS qui veulent aller plus loin, intégrer Sentry ou Better Stack avec capture des erreurs côté client donne une visibilité sur les échecs réels vécus par les utilisateurs. Les patterns d’erreurs récurrents (par exemple, échecs systématiques sur certaines IPs ouest-africaines) révèlent souvent des problèmes spécifiques de routing réseau qui peuvent être atténués en orientant le DNS via Cloudflare avec géolocalisation.

Besoin d'un site web ?

Confiez-nous la Création de Votre Site Web

Site vitrine, e-commerce ou application web — nous transformons votre vision en réalité digitale. Accompagnement personnalisé de A à Z.

À partir de 250.000 FCFA
Parlons de Votre Projet
Publicité