Como implementar upload de arquivo com fetch/axios | FormData · Barra de progresso · Tratamento de erros
Ao implementar upload de arquivos em aplicações web modernas, tornou-se padrão usar a API <code>fetch</code> ou <code>axios</code> para enviar arquivos de forma assíncrona. Embora uma simples transmissão possa ser implementada em poucas linhas, quando se incluem barra de progresso, tratamento de erros, divisão em chunks para arquivos grandes e suporte a CORS, há muitos pontos a considerar. Este artigo explica esses aspectos sistematicamente.
Como usar FormData (append / set)
<code>FormData</code> é uma API para construir dados equivalentes ao <code>enctype="multipart/form-data"</code> de formulários HTML usando JavaScript. Você pode lidar com dados de formulário completos, incluindo campos de texto além de arquivos.
// 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);
});
Upload de arquivo com fetch
Ao enviar FormData com <code>fetch</code>, o importante é <strong>não definir manualmente o header 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 que não deve configurar Content-Type manualmente: requisições <code>multipart/form-data</code> requerem um parâmetro <code>boundary</code> (ex: <code>boundary=----WebKitFormBoundaryXXX</code>), que o navegador gera automaticamente com base nos dados do FormData. Se você configurar manualmente <code>Content-Type: multipart/form-data</code>, o boundary será omitido e o servidor não conseguirá fazer parse.
Exibição de progresso com fetch (ReadableStream)
Com a Fetch API, o progresso de recepção de respostas pode ser obtido com <code>ReadableStream</code>, mas <strong>o progresso de upload não pode ser obtido apenas com fetch</strong>. Se o progresso de upload for necessário, use axios ou 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);
}
Upload com axios + implementação de barra de progresso com onUploadProgress
axios pode obter o progresso do upload através do callback <code>onUploadProgress</code>. Como envolve XMLHttpRequest, você pode usar os eventos de progresso de upload do 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);
}
}
}
Upload em chunks de arquivos grandes
Para arquivos com tamanho que não pode ser enviado em uma única solicitação (centenas de MB a GB), existe um método de dividir em chunks (pequenos pedaços) para envio. O servidor recebe os chunks e os combina no 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();
}
Tratamento de erros (413 / Erro de rede / Timeout)
Trataremos erros que podem ocorrer no upload de arquivo por padrão.
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; // それ以外は再スロー
}
}
Pontos-chave de configuração de CORS
Ao fazer upload para APIs de diferentes origens (domínio/porta), é necessário configurar CORS (Cross-Origin Resource Sharing). Solicitações POST contendo <code>multipart/form-data</code> não são 「solicitações simples」 e resultarão em uma solicitação de pré-verificação (método <code>OPTIONS</code>).
// フロントエンド側:特別な設定は不要(ブラウザが自動でプリフライトを送信)
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;
}
Pontos que requerem atenção especial com CORS em relação ao upload de arquivo:
- Se <code>Content-Type</code> estiver incluído em <code>Access-Control-Allow-Headers</code>, pode haver mal-entendido de que o navegador pode definir Content-Type livremente
- No caso de FormData, como o cabeçalho Content-Type não é configurado customicamente, às vezes o preflight não ocorre
- Quando o token de autenticação é enviado no cabeçalho <code>Authorization</code>, sempre ocorre uma solicitação de pré-voo
Arquivo de teste disponível para usar neste artigo (gratuito)
- → <a href="/ja/files/threshold/" class="text-primary-600 dark:text-primary-400 hover:underline">Lista de arquivos para teste de valores limite</a> — Verificar o comportamento de fetch/axios com arquivos de vários tamanhos
- → <a href="/ja/files/images/" class="text-primary-600 dark:text-primary-400 hover:underline">Lista de imagens de teste</a> — Testar uploads com diversos formatos de imagem como JPEG, PNG, WebP
Artigos relacionados
- → <a href="/ja/blog/php-file-upload/" class="text-primary-600 dark:text-primary-400 hover:underline">Como implementar upload de arquivos em PHP | Guia completo de validação, armazenamento e segurança</a>
- → <a href="/ja/blog/multipart-form-data-overhead/" class="text-primary-600 dark:text-primary-400 hover:underline">Calcular com precisão o 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 verificação de implementação de validação de arquivo para formulários web</a>