Qu'est-ce que multipart/form-data Content-Type ? Envoi de fichiers, exemples

multipart/form-data est le Content-Type HTTP pour les formulaires avec fichiers. Format du boundary, envoi avec fetch, curl et k6, erreurs courantes.

Qu'est-ce que multipart/form-data ?

multipart/form-data est le Content-Type HTTP utilisé pour envoyer un formulaire qui contient des fichiers ou des données binaires. Le corps de la requête est découpé en parties, une par champ, séparées par une chaîne appelée boundary, et chaque partie porte ses propres en-têtes. Les navigateurs l'utilisent lorsqu'un formulaire a enctype="multipart/form-data", et il est défini dans la RFC 7578.

Il existe parce que les octets bruts d'un fichier ne se glissent pas sans risque dans le texte key=value&key=value de application/x-www-form-urlencoded. Avec multipart, champs texte et fichiers binaires voyagent dans une seule requête.

Comment fonctionne multipart/form-data

L'en-tête Content-Type indique un boundary, une chaîne qui ne doit apparaître nulle part dans le contenu. Chaque partie commence par -- suivi du boundary, et le corps se termine par --, le boundary, puis encore --.

POST /api/upload HTTP/1.1
Host: api.example.com
Content-Type: multipart/form-data; boundary=----example7MA4YW

------example7MA4YW
Content-Disposition: form-data; name="username"

alice
------example7MA4YW
Content-Disposition: form-data; name="avatar"; filename="photo.jpg"
Content-Type: image/jpeg

(binary JPEG bytes)
------example7MA4YW--
  • Chaque partie a besoin d'un en-tête Content-Disposition: form-data avec le name du champ. Les parties de fichier ajoutent un filename.
  • Le Content-Type propre à une partie est facultatif. En son absence, la partie est lue comme text/plain. Les fichiers doivent indiquer leur type de média, ou application/octet-stream s'il est inconnu.
  • N'ajoutez pas Content-Transfer-Encoding. La RFC 7578 précise que les émetteurs ne doivent pas l'utiliser en HTTP.

multipart/form-data face à urlencoded et JSON

  • multipart/form-data gère les fichiers et les champs binaires, au prix d'un surcoût de boundary et d'en-têtes par partie.
  • application/x-www-form-urlencoded est la valeur par défaut des formulaires texte seul. Compact, mais inadapté aux fichiers.
  • application/json est le choix habituel pour les requêtes d'API texte seul. Il ne gère pas les fichiers directement : un envoi passe par multipart, une étape d'envoi distincte ou du base64 dans le JSON (ce qui alourdit les données d'environ un tiers).

Pour mieux situer ce que peut contenir un corps de requête, voir payloads d'API et JSON.

Envoyer du multipart/form-data depuis du code

Navigateur (fetch et FormData)

const fd = new FormData();
fd.append('username', 'alice');
fd.append('avatar', fileInput.files[0]);

await fetch('/api/upload', { method: 'POST', body: fd });

Ne définissez pas vous-même l'en-tête Content-Type ici. MDN avertit que le navigateur ne peut alors plus y inscrire le boundary qu'il a généré, et le serveur ne peut plus découper le corps.

curl

curl -X POST https://api.example.com/upload \
  -F "username=alice" \
  -F "avatar=@photo.jpg"

-F fait envoyer du multipart/form-data par curl, et le préfixe @ lit la valeur depuis un fichier. D'autres options figurent dans la commande curl, et vous pouvez essayer une requête sans rien installer avec l'outil gratuit curl online.

Python (requests)

import requests

requests.post(
    'https://api.example.com/upload',
    data={'username': 'alice'},
    files={'avatar': open('photo.jpg', 'rb')},
)

k6

import http from 'k6/http';

const img = open('./photo.jpg', 'b');

export default function () {
  http.post('https://api.example.com/upload', {
    username: 'alice',
    avatar: http.file(img, 'photo.jpg', 'image/jpeg'),
  });
}

Passer http.file() dans un objet de corps est la façon dont k6 envoie un fichier. Chargez le fichier une seule fois dans la phase d'initialisation avec open(path, 'b'), pas dans la fonction par défaut.

La lecture côté serveur

Le serveur a besoin d'un parseur multipart : Multer pour Express, request.FILES dans Django, request.files dans Flask, UploadFile dans FastAPI, MultipartFile dans Spring. Les parseurs diffèrent : certains gardent les fichiers en mémoire, d'autres les écrivent sur disque au fil de l'eau. Vérifiez-le avant d'accepter de gros envois.

Considérez tout ce que contient une partie comme non fiable. La RFC 7578 demande aux récepteurs de ne pas utiliser un nom de fichier tel quel et d'écarter tout chemin de répertoire qu'il contient. Le Content-Type d'une partie n'est lui aussi qu'une déclaration du client : déterminez le vrai type du fichier à partir de son contenu.

Erreurs courantes

  • 413 Content Too Large. Le corps dépasse une limite du serveur ou du proxy. nginx fixe client_max_body_size à 1m par défaut : une photo ordinaire échoue tant que vous n'augmentez pas cette valeur.
  • 400 pour boundary manquant. L'en-tête Content-Type n'avait pas de paramètre boundary, ou il ne correspondait pas au corps. C'est souvent dû à un en-tête défini à la main.
  • Le champ fichier arrive vide. Le nom du champ dans le formulaire ou le code ne correspond pas à celui attendu par le serveur, ou le client a écrasé le boundary.
  • Noms de fichier déformés. Les noms non ASCII demandent un UTF-8 cohérent. La RFC 7578 autorise l'encodage en pourcentage dans les noms de fichier et déconseille ici la forme filename* de la RFC 5987.
  • Envois qui calent sur des liaisons lentes. Pour les très gros fichiers, mieux vaut un envoi direct vers un stockage objet avec une URL signée, ou un protocole reprenable, plutôt que de passer par votre serveur d'API.

Tester les envois multipart sous charge

  • Utilisez des tailles et des contenus de fichier réalistes. De minuscules fichiers factices masquent le coût en bande passante et en traitement, et des octets aléatoires ne se compressent pas comme des images ou des PDF.
  • Testez juste en dessous et juste au-dessus de la limite de taille, et vérifiez qu'un 413 propre est renvoyé, pas un 500.
  • Lancez des envois en parallèle pour voir si le serveur garde les fichiers en mémoire et finit par en manquer.
  • Testez depuis les régions de vos utilisateurs, car la vitesse d'envoi dépend beaucoup de la distance.

LoadFocus exécute des scripts JMeter et k6 depuis 25 régions cloud : un script d'envoi comme l'exemple k6 ci-dessus peut donc partir de plusieurs endroits à la fois. Pour partir d'une commande cURL avec des options -F, voir transformer cURL, HAR, Postman ou OpenAPI en test de charge (en anglais). Pour voir comment les paramètres changent selon le type de corps, lisez How are Parameters Sent in an HTTP POST Request? (en anglais). Ce qui est dit sur les tests d'API vaut aussi pour les endpoints d'envoi.

Questions fréquentes sur multipart/form-data

Pourquoi le boundary est-il dans l'en-tête Content-Type ?

Le parseur a besoin du boundary avant de pouvoir lire le corps, et il ne peut pas le chercher dans un corps qu'il n'a pas encore découpé. Dans l'en-tête, il est disponible dès le départ.

Puis-je utiliser multipart/form-data sans fichiers ?

Oui, cela fonctionne aussi avec des champs texte seul. Pour des requêtes texte seul, urlencoded ou JSON sont plus légers, car il n'y a pas de surcoût par partie.

Quelle différence entre multipart/form-data et multipart/mixed ?

multipart/form-data sert aux soumissions de formulaire, et chaque partie porte un Content-Disposition avec le nom du champ. multipart/mixed est le conteneur générique utilisé dans les e-mails et d'autres messages MIME.

Comment envoyer du JSON et un fichier ensemble ?

Envoyez deux parties dans une même requête multipart : une partie texte contenant le JSON (avec son propre Content-Type application/json) et une partie fichier. Intégrer le fichier en base64 dans le JSON fonctionne aussi, mais alourdit les données d'environ un tiers.

Pourquoi mon envoi échoue-t-il si je définis le Content-Type à la main ?

Parce que la valeur n'a pas de boundary, ou qu'il ne correspond pas au corps. Laissez l'en-tête non défini et laissez votre client HTTP ou le navigateur le générer.

Quelle est la taille maximale d'un envoi ?

HTTP n'en fixe aucune. La limite vient de votre serveur, proxy ou passerelle : nginx utilise 1m par défaut et on peut l'augmenter avec client_max_body_size. Pour de très gros fichiers, utilisez des URL signées vers un stockage objet ou un envoi reprenable.

Quelle est la vitesse de votre site web?

Augmentez sa vitesse et son référencement naturel de manière transparente avec notre Test de Vitesse gratuit.

Test gratuit de vitesse du site Web

Analyser la vitesse de chargement de votre site Web et améliorer ses performances avec notre outil gratuit de vérification de la vitesse de la page.