Troubleshooting: File Upload Issues
Solusi untuk masalah upload avatar, gambar, dan file ke Cloudflare R2.
🔴 Error: "Upload failed" atau 413 Payload Too Large
Penyebab
File terlalu besar (limit default: 5MB untuk avatar).
Solusi
1. Check File Size Limit
typescript
// src/routes/api/upload/image/+server.ts
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
export const POST = async ({ request }) => {
const form = await request.formData();
const file = form.get('file') as File;
if (file.size > MAX_SIZE) {
return json(
{ error: 'File terlalu besar. Maksimal 5MB.' },
{ status: 413 }
);
}
// ...
};2. Compress Image Sebelum Upload
typescript
// Client-side compression
async function compressImage(file: File): Promise<Blob> {
return new Promise((resolve, reject) => {
const img = new Image();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
img.onload = () => {
// Resize jika terlalu besar
const maxWidth = 1200;
const scale = Math.min(1, maxWidth / img.width);
canvas.width = img.width * scale;
canvas.height = img.height * scale;
ctx?.drawImage(img, 0, 0, canvas.width, canvas.height);
canvas.toBlob(
(blob) => blob ? resolve(blob) : reject('Failed'),
'image/webp',
0.85 // Quality
);
};
img.src = URL.createObjectURL(file);
});
}3. Use Presigned URL untuk File Besar
typescript
// Upload langsung ke R2 untuk file besar (>5MB)
const res = await fetch('/api/upload/presign', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: 'large-file.pdf',
contentType: 'application/pdf'
})
});
const { uploadUrl } = await res.json();
// Upload langsung ke R2
await fetch(uploadUrl, {
method: 'PUT',
body: file,
headers: { 'Content-Type': 'application/pdf' }
});🔴 Error: "Invalid file type"
Penyebab
File type tidak diizinkan atau tidak terdeteksi dengan benar.
Solusi
1. Check Allowed Types
typescript
// src/routes/api/upload/image/+server.ts
const ALLOWED_TYPES = [
'image/jpeg',
'image/png',
'image/webp',
'image/gif'
];
export const POST = async ({ request }) => {
const file = form.get('file') as File;
if (!ALLOWED_TYPES.includes(file.type)) {
return json(
{ error: `Tipe file ${file.type} tidak diizinkan` },
{ status: 400 }
);
}
// ...
};2. Validate dengan Magic Numbers
File type bisa di-spoof. Validate dengan file signature:
typescript
async function validateFileType(file: File): Promise<boolean> {
const buffer = await file.slice(0, 4).arrayBuffer();
const bytes = new Uint8Array(buffer);
// JPEG: FF D8 FF
if (bytes[0] === 0xFF && bytes[1] === 0xD8) {
return true;
}
// PNG: 89 50 4E 47
if (bytes[0] === 0x89 && bytes[1] === 0x50) {
return true;
}
// WebP: 52 49 46 46 (RIFF header)
if (bytes[0] === 0x52 && bytes[1] === 0x49) {
return true;
}
return false;
}🔴 Error: "R2 binding not found"
Penyebab
Binding R2 tidak terkonfigurasi dengan benar.
Solusi
1. Check wrangler.toml
toml
[[r2_buckets]]
binding = "STORAGE" # Nama binding yang digunakan di code
bucket_name = "your-bucket-name"2. Dashboard Binding
- Buka Cloudflare Dashboard → Pages → Your Project
- Settings → R2 Buckets
- Add Binding:
- Variable name:
STORAGE - R2 bucket: Select your bucket
- Variable name:
3. Test R2 Access
typescript
// Test endpoint
export const GET = async ({ platform }) => {
try {
const objects = await platform.env.STORAGE.list();
return json({ buckets: objects.objects.map(o => o.key) });
} catch (error) {
return json({ error: error.message }, { status: 500 });
}
};🔴 Error: "Presigned URL expired"
Penyebab
Presigned URL hanya valid untuk waktu terbatas (default: 5 menit).
Solusi
1. Generate URL sebelum Upload
typescript
// Generate presigned URL just-in-time
const getPresignedUrl = async (filename: string) => {
const res = await fetch('/api/upload/presign', {
method: 'POST',
body: JSON.stringify({ filename })
});
const { uploadUrl } = await res.json();
// Upload segera
await fetch(uploadUrl, {
method: 'PUT',
body: file
});
};2. Extend Expiry Time (jika perlu)
typescript
// src/lib/storage/r2.ts
export async function generatePresignedUrl(
filename: string,
expiresIn: number = 15 * 60 // 15 menit
) {
const command = new PutObjectCommand({
Bucket: R2_BUCKET_NAME,
Key: filename,
ContentType: 'application/octet-stream'
});
return getSignedUrl(s3Client, command, { expiresIn });
}🔴 Image Tidak Ter-load setelah Upload
Penyebab & Solusi
1. Check R2 Public URL
typescript
// Pastikan R2_PUBLIC_URL di .env
R2_PUBLIC_URL=https://pub-yourid.r2.dev2. Object Access Control
typescript
// Pastikan object public
await platform.env.STORAGE.put(filename, file, {
httpMetadata: {
contentType: file.type,
cacheControl: 'public, max-age=31536000'
}
});3. CORS Configuration
json
// R2 Bucket CORS Rules
[
{
"AllowedOrigins": ["https://yourdomain.com"],
"AllowedMethods": ["GET", "HEAD"],
"AllowedHeaders": ["*"],
"MaxAgeSeconds": 3600
}
]🔴 Upload Progress Tidak Muncul
Solusi: Implementasi dengan XMLHttpRequest
svelte
<script>
let progress = 0;
let uploading = false;
async function uploadWithProgress(file: File) {
uploading = true;
progress = 0;
const xhr = new XMLHttpRequest();
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
progress = Math.round((e.loaded / e.total) * 100);
}
};
return new Promise((resolve, reject) => {
xhr.onload = () => resolve(xhr.response);
xhr.onerror = reject;
xhr.open('POST', '/api/upload/image');
const formData = new FormData();
formData.append('file', file);
xhr.send(formData);
});
}
</script>
{#if uploading}
<div class="progress-bar">
<div class="progress" style="width: {progress}%"></div>
</div>
<p>{progress}%</p>
{/if}🔴 WebP Conversion Failed
Penyebab
Browser tidak support WebP atau conversion error.
Solusi: Fallback
typescript
async function convertToWebP(file: File): Promise<Blob> {
// Check WebP support
const hasWebPSupport = document
.createElement('canvas')
.toDataURL('image/webp')
.startsWith('data:image/webp');
if (!hasWebPSupport) {
// Return original jika tidak support
return file;
}
try {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
img.src = URL.createObjectURL(file);
});
canvas.width = img.width;
canvas.height = img.height;
ctx?.drawImage(img, 0, 0);
const webpBlob = await new Promise<Blob>((resolve, reject) => {
canvas.toBlob(
(blob) => blob ? resolve(blob) : reject('Conversion failed'),
'image/webp',
0.85
);
});
return webpBlob;
} catch (error) {
console.error('WebP conversion failed:', error);
return file; // Fallback to original
}
}🔴 Multiple File Upload Issues
Solusi: Queue Upload
svelte
<script>
let files: File[] = [];
let uploadQueue = [];
async function uploadMultiple(files: File[]) {
uploadQueue = files.map(file => ({
file,
status: 'pending',
progress: 0
}));
// Upload sequentially atau parallel dengan limit
const CONCURRENT_UPLOADS = 3;
for (let i = 0; i < uploadQueue.length; i += CONCURRENT_UPLOADS) {
const batch = uploadQueue.slice(i, i + CONCURRENT_UPLOADS);
await Promise.all(
batch.map(item => uploadFile(item))
);
}
}
async function uploadFile(item) {
item.status = 'uploading';
try {
const formData = new FormData();
formData.append('file', item.file);
const res = await fetch('/api/upload/image', {
method: 'POST',
body: formData
});
if (!res.ok) throw new Error('Upload failed');
item.status = 'completed';
} catch (error) {
item.status = 'error';
}
}
</script>🔧 Debug Upload
Test Upload dengan cURL
bash
# Test image upload
curl -X POST http://localhost:5173/api/upload/image \
-F "file=@test-image.jpg" \
-F "type=avatar"
# Test dengan cookies (authenticated)
curl -X POST http://localhost:5173/api/upload/image \
-b cookies.txt \
-F "file=@test-image.jpg"Check R2 Bucket Contents
bash
# List objects
npx wrangler r2 object list your-bucket-name
# Check specific object
npx wrangler r2 object get your-bucket-name uploads/avatar-xxx.webp🆘 Quick Fixes
Clear Upload Errors
svelte
<script>
let error = null;
function clearError() {
error = null;
}
async function handleUpload(file: File) {
try {
error = null;
await uploadFile(file);
} catch (e) {
error = e.message;
}
}
</script>
{#if error}
<div class="error">
{error}
<button on:click={clearError}>Coba Lagi</button>
</div>
{/if}