So implementieren Sie Datei-Upload mit fetch/axios | FormData · Fortschrittsleiste · Fehlerbehandlung
Bei der Implementierung von Datei-Uploads in modernen Webanwendungen ist es Standard geworden, die <code>fetch</code> API oder <code>axios</code> zu verwenden, um Dateien asynchron zu senden. Während ein einfaches Senden in wenigen Zeilen implementiert werden kann, gibt es viele Punkte zu beachten, wenn man Fortschrittsbalken, Fehlerbehandlung, Chunk-Aufteilung für große Dateien und CORS-Unterstützung einbezieht. Dieser Artikel erklärt diese Aspekte systematisch.
Verwendung von FormData (append / set)
<code>FormData</code> ist eine API, um Daten equivalent zu <code>enctype="multipart/form-data"</code> von HTML-Formularen mit JavaScript zu konstruieren. Sie können damit komplette Formulardaten inklusive Textfelder und Dateien handhaben.
// 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);
});
Datei-Upload mit fetch
Beim Senden von FormData mit <code>fetch</code> ist wichtig, dass Sie <strong>den Content-Type-Header nicht manuell setzen dürfen</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);
}
});
Warum Sie Content-Type nicht manuell setzen sollten: Anfragen mit <code>multipart/form-data</code> benötigen einen <code>boundary</code>-Parameter (z. B. <code>boundary=----WebKitFormBoundaryXXX</code>), den der Browser automatisch basierend auf den FormData-Daten generiert. Wenn Sie <code>Content-Type: multipart/form-data</code> manuell setzen, fehlt der boundary und der Server kann nicht parsen.
Fortschrittsanzeige mit fetch (ReadableStream)
Mit der Fetch API kann der Fortschritt beim Empfang von Antworten mit <code>ReadableStream</code> abgerufen werden, aber <strong>der Upload-Fortschritt kann nicht mit fetch allein abgerufen werden</strong>. Wenn Upload-Fortschritt erforderlich ist, verwenden Sie axios oder 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 mit axios + Implementierung einer Fortschrittsleiste mit onUploadProgress
axios kann den Upload-Fortschritt über den <code>onUploadProgress</code>-Callback abrufen. Da es XMLHttpRequest umhüllt, können Sie die Upload-Fortschrittsereignisse des Browsers nutzen.
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);
}
}
}
Chunked Upload großer Dateien
Für Dateien mit einer Größe, die nicht in einer einzelnen Anforderung gesendet werden kann (Hunderte von MB bis GB), gibt es eine Methode, sie in Chunks (kleine Datenblöcke) zu unterteilen. Der Server empfängt die Chunks und kombiniert sie am Ende.
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();
}
Fehlerbehandlung (413 / Netzwerkfehler / Timeout)
Wir behandeln Fehler, die beim Datei-Upload auftreten können, nach Muster.
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; // それ以外は再スロー
}
}
Wichtige Punkte der CORS-Konfiguration
Beim Hochladen zu APIs aus unterschiedlichen Quellen (Domain/Port) ist eine CORS-Konfiguration (Cross-Origin Resource Sharing) erforderlich. POST-Anfragen mit <code>multipart/form-data</code> sind keine 「einfachen Anfragen」 und lösen eine Preflight-Anfrage (OPTIONS-Methode) aus.
// フロントエンド側:特別な設定は不要(ブラウザが自動でプリフライトを送信)
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;
}
Punkte, auf die bei CORS im Zusammenhang mit Datei-Uploads besonders zu achten ist:
- Wenn <code>Content-Type</code> in <code>Access-Control-Allow-Headers</code> enthalten ist, kann es zu Missverständnissen führen, dass der Browser Content-Type frei setzen kann
- Im Fall von FormData tritt kein Preflight auf, da der Content-Type-Header nicht benutzerdefiniert konfiguriert ist
- Wenn das Authentifizierungstoken im <code>Authorization</code>-Header gesendet wird, tritt immer eine Preflight-Anfrage auf
Testdatei zur Verwendung in diesem Artikel (kostenlos)
- → <a href="/ja/files/threshold/" class="text-primary-600 dark:text-primary-400 hover:underline">Dateien für Grenzwerttest</a> — Verhalten von fetch/axios mit verschiedenen Dateigröße validieren
- → <a href="/ja/files/images/" class="text-primary-600 dark:text-primary-400 hover:underline">Liste der Testbilder</a> — Upload mit verschiedenen Bildformaten wie JPEG, PNG, WebP testen
Verwandte Artikel
- → <a href="/ja/blog/php-file-upload/" class="text-primary-600 dark:text-primary-400 hover:underline">So implementieren Sie Datei-Upload in PHP | Vollständiger Leitfaden zu Validierung, Speicherung und Sicherheit</a>
- → <a href="/ja/blog/multipart-form-data-overhead/" class="text-primary-600 dark:text-primary-400 hover:underline">Den Overhead von multipart/form-data genau berechnen</a>
- → <a href="/ja/blog/file-validation-checklist/" class="text-primary-600 dark:text-primary-400 hover:underline">Checkliste zur Implementierung der Dateiverifizierung für Web-Formulare</a>