Cómo implementar carga de archivos con fetch/axios | FormData, barra de progreso, manejo de errores
Al implementar carga de archivos en aplicaciones web modernas, se ha convertido en estándar enviar archivos de forma asincrónica utilizando la API <code>fetch</code> o <code>axios</code>. Aunque la transmisión simple se puede implementar en solo unas pocas líneas, considerar barras de progreso, manejo de errores, fragmentación de archivos grandes y soporte CORS requiere atención a muchos factores. Este artículo explica sistemáticamente todos estos.
Cómo usar FormData (append / set)
<code>FormData</code> es una API para construir en JavaScript datos equivalentes a <code>enctype="multipart/form-data"</code> en formularios HTML. Puede manejar datos de formulario completos incluyendo no solo archivos sino también campos de texto.
// input[type="file"] からファイルを取得
const fileInput = document.querySelector('#file-input');
const file = fileInput.files[0];
// FormData を作成
const formData = new FormData();
// append: 同名のキーで複数の値を追加できる
formData.append('file', file);
formData.append('file', anotherFile); // 複数ファイルを同じキーで送れる
formData.append('description', 'アップロードテスト');
formData.append('userId', '12345');
// set: 既存のキーがあれば上書き(追加ではなく置換)
formData.set('file', newFile); // 既存の 'file' エントリをすべて削除して置換
// 中身の確認
for (const [key, value] of formData.entries()) {
console.log(key, value);
}
// 複数ファイルを input[type="file" multiple] から追加する場合
const multipleFiles = fileInput.files;
Array.from(multipleFiles).forEach(f => {
formData.append('files[]', f);
});
Carga de archivo con fetch
Cuando se envía FormData con <code>fetch</code>, lo importante es <strong>no establecer manualmente el encabezado Content-Type</strong>.
async function uploadWithFetch(file) {
const formData = new FormData();
formData.append('file', file);
formData.append('name', file.name);
// NG: Content-Type を手動で設定してはいけない
// fetch('/upload', {
// method: 'POST',
// headers: { 'Content-Type': 'multipart/form-data' }, // ← これは誤り!
// body: formData,
// });
// OK: Content-Type は fetch が自動で設定する(boundary パラメータも含む)
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
// headers は指定しない(または Content-Type 以外のヘッダーのみ指定)
});
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }));
throw new Error(`Upload failed: ${response.status} - ${error.message}`);
}
return response.json();
}
// 使用例
const fileInput = document.querySelector('#file-input');
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
const result = await uploadWithFetch(file);
console.log('アップロード成功:', result);
} catch (err) {
console.error('アップロード失敗:', err.message);
}
});
Por qué no debe configurar Content-Type manualmente: las solicitudes <code>multipart/form-data</code> requieren un parámetro <code>boundary</code> (por ejemplo, <code>boundary=----WebKitFormBoundaryXXX</code>), que el navegador genera automáticamente en función de FormData. Si configura manualmente <code>Content-Type: multipart/form-data</code>, faltará el boundary y el servidor no podrá analizarlo.
Visualización del progreso con fetch (ReadableStream)
Con la API fetch, el progreso de recepción de respuesta se puede obtener a través de <code>ReadableStream</code>, pero <strong>el progreso de carga no se puede obtener solo con fetch</strong>. Si se necesita el progreso de carga, use axios o XMLHttpRequest.
// fetch でダウンロード進捗を取得する例(アップロードとは逆方向)
async function downloadWithProgress(url, onProgress) {
const response = await fetch(url);
const contentLength = response.headers.get('Content-Length');
const total = parseInt(contentLength, 10);
let loaded = 0;
const reader = response.body.getReader();
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
loaded += value.length;
onProgress(loaded, total);
}
return new Blob(chunks);
}
Carga de archivo con axios + Implementación de barra de progreso con onUploadProgress
axios puede recuperar el progreso de carga a través de la devolución de llamada <code>onUploadProgress</code>. Dado que envuelve XMLHttpRequest, puedes utilizar los eventos de progreso de carga del navegador.
import axios from 'axios';
async function uploadWithAxios(file, onProgress) {
const formData = new FormData();
formData.append('file', file);
const response = await axios.post('/api/upload', formData, {
headers: {
// axios は FormData を自動検出して Content-Type を設定するが、
// 明示的に undefined を指定して axios に任せることもできる
},
onUploadProgress: (progressEvent) => {
if (progressEvent.total) {
const percent = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
onProgress(percent);
}
},
// タイムアウト設定(ミリ秒)
timeout: 300000, // 5分
});
return response.data;
}
// プログレスバーと組み合わせた使用例
const progressBar = document.querySelector('#progress-bar');
const progressText = document.querySelector('#progress-text');
async function handleUpload(file) {
try {
const result = await uploadWithAxios(file, (percent) => {
progressBar.style.width = `${percent}%`;
progressBar.setAttribute('aria-valuenow', percent);
progressText.textContent = `${percent}%`;
});
console.log('完了:', result);
} catch (err) {
if (axios.isAxiosError(err)) {
console.error('ステータス:', err.response?.status);
console.error('メッセージ:', err.response?.data?.message);
} else {
console.error('予期しないエラー:', err);
}
}
}
Carga dividida en fragmentos para archivos grandes
Para archivos demasiado grandes para enviar en una sola solicitud (cientos de MB a GB), puede dividirlos en fragmentos (piezas pequeñas) y enviarlos por separado. El servidor recibe cada fragmento y los combina al final.
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MiB ずつ分割
async function uploadInChunks(file, onProgress) {
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
const uploadId = crypto.randomUUID(); // アップロードセッションID
for (let i = 0; i < totalChunks; i++) {
const start = i * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('uploadId', uploadId);
formData.append('chunkIndex', String(i));
formData.append('totalChunks', String(totalChunks));
formData.append('filename', file.name);
await fetch('/api/upload/chunk', {
method: 'POST',
body: formData,
});
// 各チャンク送信後に進捗を更新
const percent = Math.round(((i + 1) / totalChunks) * 100);
onProgress(percent);
}
// 全チャンク送信後にサーバーへ結合を指示
const response = await fetch('/api/upload/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ uploadId, filename: file.name }),
});
return response.json();
}
Manejo de errores (413 / error de red / tiempo de espera)
Manejar errores que pueden ocurrir durante la carga de archivos por patrón.
async function robustUpload(file) {
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
signal: AbortSignal.timeout(60000), // 60秒でタイムアウト
});
// ステータスコード別のエラー処理
if (response.status === 413) {
throw new Error('ファイルサイズが大きすぎます。サーバーの上限を超えています。');
}
if (response.status === 415) {
throw new Error('サポートされていないファイル形式です。');
}
if (response.status === 422) {
const data = await response.json();
throw new Error(`バリデーションエラー: ${data.message}`);
}
if (!response.ok) {
throw new Error(`サーバーエラー: ${response.status} ${response.statusText}`);
}
return await response.json();
} catch (err) {
if (err.name === 'AbortError' || err.name === 'TimeoutError') {
throw new Error('アップロードがタイムアウトしました。ネットワーク接続を確認してください。');
}
if (err instanceof TypeError && err.message.includes('Failed to fetch')) {
throw new Error('ネットワークエラーが発生しました。接続を確認してください。');
}
throw err; // それ以外は再スロー
}
}
Puntos clave de configuración CORS
Cuando carga a una API en un origen diferente (dominio o puerto), se requiere configuración de CORS (Cross-Origin Resource Sharing). Las solicitudes POST que contienen multipart/form-data no son "solicitudes simples" y desencadenarán una solicitud previa (método OPTIONS).
// フロントエンド側:特別な設定は不要(ブラウザが自動でプリフライトを送信)
const response = await fetch('https://api.example.com/upload', {
method: 'POST',
body: formData,
// credentials: 'include', // クッキーを含む場合(サーバー側で credentials: true が必要)
});
# Nginx でのCORS設定例
location /api/upload {
# プリフライトリクエスト(OPTIONS)への応答
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://frontend.example.com';
add_header 'Access-Control-Allow-Methods' 'POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type';
add_header 'Access-Control-Max-Age' '86400';
add_header 'Content-Length' '0';
return 204;
}
add_header 'Access-Control-Allow-Origin' 'https://frontend.example.com';
proxy_pass http://app_backend;
}
Puntos que requieren especial atención de CORS con respecto a cargas de archivos:
- incluir <code>Content-Type</code> en <code>Access-Control-Allow-Headers</code> puede generar malentendidos de que el navegador puede establecer Content-Type libremente
- Con FormData, es posible que no se produzcan solicitudes preflight porque el encabezado Content-Type no se configura de forma personalizada
- Cuando se envía un token de autenticación en el encabezado <code>Authorization</code>, siempre ocurre una solicitud de verificación previa.
Archivos de prueba para este artículo (gratis)
- → <a href="/ja/files/threshold/" class="text-primary-600 dark:text-primary-400 hover:underline">Lista de archivos de prueba de valores límite</a> — Verificar el comportamiento de fetch/axios con archivos de varios tamaños
- → <a href="/ja/files/images/" class="text-primary-600 dark:text-primary-400 hover:underline">Lista de imágenes de prueba</a> — Pruebe cargas con varios formatos de imagen como JPEG, PNG y WebP
Artículos relacionados
- → <a href="/ja/blog/php-file-upload/" class="text-primary-600 dark:text-primary-400 hover:underline">Cómo Implementar la Carga de Archivos en PHP | Guía Completa de Validación, Almacenamiento y Seguridad</a>
- → <a href="/ja/blog/multipart-form-data-overhead/" class="text-primary-600 dark:text-primary-400 hover:underline">Calcular con Precisión el Overhead de multipart/form-data</a>
- → <a href="/ja/blog/file-validation-checklist/" class="text-primary-600 dark:text-primary-400 hover:underline">Lista de Verificación de Implementación de Validación de Archivos en Formularios Web</a>