Development

Fix Error ERR_UPLOAD_FILE_CHANGED di Next.js Production

Asep Alazhari

Solve error misterius file upload yang cuma muncul di production. Pelajari kenapa FormData rebuild dan file references bikin upload gagal di aplikasi Next.js.

Fix Error ERR_UPLOAD_FILE_CHANGED di Next.js Production

Misteri File yang Tiba-Tiba Hilang

Lo baru aja deploy aplikasi Next.js ke production, dan semuanya keliatan sempurna. Fitur file upload lo jalan mulus di localhost, users bisa drag and drop gambar, paste screenshots, bahkan copy file dari desktop mereka. Lo udah test berkali-kali. Lo tidur dengan perasaan puas banget.

Terus tiba-tiba pesan-pesan mulai berdatangan. “Gue ga bisa upload file nih.” “Tombol submit-nya ga jalan.” “Terus-terusan gagal dengan error aneh.” Lo check browser console dan ada nih, nge-mock lo dengan tulisan merah: net::ERR_UPLOAD_FILE_CHANGED.

Error ERR_UPLOAD_FILE_CHANGED di browser console

Browser Network tab yang nunjukin error ERR_UPLOAD_FILE_CHANGED yang cuma muncul di production - issue frustasi yang bisa bikin file upload functionality rusak

Tapi nih yang bikin pusing, lo coba sendiri di localhost, dan itu jalan sempurna. Lagi. Dan lagi. Error-nya cuma muncul di production, dan itu bikin lo gila.

Gue pernah ngalamin ini. Minggu lalu, gue ngabisin berjam-jam debugging issue ini di sistem budget management yang lagi gue bikin. File upload-nya jalan mulus banget di development, tapi users di production ngalamin kegagalan acak waktu submit activity forms dengan evidence files. Beberapa uploads sukses, yang lain gagal secara misterius. Pola-nya frustratingly inconsistent.

Setelah menyelam dalam ke perilaku browser, FormData lifecycle, dan Next.js production configurations, gue nemuin sesuatu yang menarik: ini bukan cuma bug, tapi badai sempurna dari browser security, file system references, dan production infrastructure yang ga ada di development.

Kalo lo lagi baca ini karena ngalamin mimpi buruk yang sama, lo di tempat yang tepat nih. Yuk kita solve bareng.

Memahami Root Cause-nya

File Object yang Punya Dua Wajah

Error ERR_UPLOAD_FILE_CHANGED terjadi ketika browser detect bahwa File object contents-nya berubah antara waktu dia di-select dan waktu dia actually di-upload. Tapi kenapa file-nya bisa berubah? Lo kan ga nge-modify itu, kan?

Nih yang bikin kejutan: bukan soal file-nya berubah, tapi tentang gimana File object merujuk datanya.

Waktu lo handle file uploads di JavaScript, lo kerja dengan File objects yang bisa merujuk data dengan dua cara yang secara fundamental berbeda:

  1. File System Reference: Nunjuk ke file di disk (dipake waktu lo copy-paste dari Finder/Explorer)
  2. In-Memory Data: Berisi byte-byte asli di browser memory (dipake buat screenshots dan beberapa drag-drop operations)

Perbedaan ini ga keliatan di development tapi jadi krusial di production environments dengan infrastruktur kompleks.

Kenapa Production Beda

Setup localhost lo sederhana: browser → Next.js dev server. Satu lompatan, tanpa redirects, tanpa proxy layers.

Production itu beda banget:

Browser → CDN → Load Balancer → Nginx Reverse Proxy → Docker Container → Next.js App

Di kasus gue, production environment pake /busdev base path, artinya setiap API request lewat beberapa redirect layers:

// next.config.ts
basePath: process.env.NODE_ENV === "production" ? "/busdev" : "";

Setiap redirect di rantai ini menciptakan kesempatan buat File references buat rusak. Waktu FormData di-rebuild saat redirect handling, browser mendeteksi bahwa File object references udah berubah dan throw ERR_UPLOAD_FILE_CHANGED.

Problem Pattern-nya

Scenario 1: Issue FormData Rebuild

Nih yang terjadi di code gue (dan mungkin di code lo juga):

// BROKEN: FormData di-rebuild di setiap retry
const submitWithRedirectSupport = async (url: string) => {
    let currentUrl = url;

    for (let attempt = 0; attempt < 3; attempt += 1) {
        const response = await fetch(currentUrl, {
            method: "POST",
            body: buildFormData(), // Creates NEW FormData setiap kali!
            credentials: "include",
            redirect: "manual",
        });

        // Handle redirect...
        if (response.status >= 300 && response.status < 400) {
            currentUrl = response.headers.get("location");
            continue; // Retry dengan URL baru
        }

        return response;
    }
};

Problem-nya? Setiap kali kita hit redirect dan retry, buildFormData() bikin FormData instance yang baru dengan File object references yang baru. Browser security mechanism mendeteksi ini dan menganggap file-nya udah diutak-atik.

Scenario 2: Issue Pasted File Reference

Tapi tunggu, masih ada lagi! Bahkan setelah fixing FormData rebuild issue, gue nemuin masalah lain: pasting files dari Finder/Explorer.

Waktu users paste images dua cara berbeda:

  • Screenshot (Cmd/Ctrl + Shift + 4, terus paste): Jalan lancar
  • Copy file dari Finder (Cmd/Ctrl + C, terus paste): Gagal dengan ERR_UPLOAD_FILE_CHANGED

Kenapa? Karena clipboardData.items[i].getAsFile() menghasilkan tipe File objects yang berbeda:

// Screenshot paste: File object dengan in-memory blob data
// File dari Finder: File object dengan file system reference - Problem!

const handlePaste = (e: ClipboardEvent) => {
    const item = e.clipboardData?.items[0];
    const file = item.getAsFile(); // Menghasilkan file system reference buat copied files

    // File di-store di state...
    setUploadedFile(file);

    // ...tapi pas upload time, rujukannya mungkin udah invalid!
};

Buat copied files, File object nunjuk ke lokasi source file. Kalo file itu pindah, dihapus, atau OS invalidate rujukannya pas lo click submit, boom, ERR_UPLOAD_FILE_CHANGED.

Solution Lengkapnya

Fix 1: Stabilize FormData Across Retries

Fix pertama gampang: build FormData sekali sebelum retry loop dan pakai ulang instance yang sama:

// FIXED: FormData di-build sekali dan di-reuse
const submitWithRedirectSupport = async (url: string) => {
    let currentUrl = url;
    const formData = buildFormData(); // Build SEKALI di luar loop

    for (let attempt = 0; attempt < 3; attempt += 1) {
        // Add exponential backoff buat retries
        if (attempt > 0) {
            const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
            await new Promise((resolve) => setTimeout(resolve, delay));
        }

        try {
            const response = await fetch(currentUrl, {
                method: "POST",
                body: formData, // Reuse FormData instance yang sama
                credentials: "include",
                redirect: "manual",
            });

            if (response.status >= 300 && response.status < 400) {
                const location = response.headers.get("location");
                if (!location) {
                    throw new Error("Redirect without location header");
                }

                currentUrl = location.startsWith("http")
                    ? location
                    : new URL(location, window.location.origin).toString();
                continue;
            }

            return response;
        } catch (error) {
            if (attempt === 2) {
                throw new Error(
                    `Upload failed after ${attempt + 1} attempts: ${
                        error instanceof Error ? error.message : "Unknown error"
                    }`
                );
            }
            console.warn(`Upload attempt ${attempt + 1} failed, retrying...`, error);
        }
    }

    throw new Error("Too many redirects");
};

Ini memastikan File object references tetap konsisten di semua retry attempts, menghilangkan redirect-based error.

Fix 2: Convert File References ke In-Memory Blobs

Fix kedua menangani paste-from-Finder scenario dengan langsung membaca file data ke memory:

// FIXED: Convert semua files ke in-memory blobs
const processFile = useCallback(
    async (file: File) => {
        // Read file data ke memory immediately
        const buffer = await file.arrayBuffer();
        const blob = new Blob([buffer], { type: file.type });

        // Create File object baru dari in-memory blob
        const stabilizedFile = new File([blob], file.name, {
            type: file.type,
            lastModified: file.lastModified,
        });

        // Sekarang file data dijamin ada di memory
        stabilizedFile.preview = URL.createObjectURL(stabilizedFile);
        onFileUpload(fileType, stabilizedFile);
    },
    [fileType, onFileUpload]
);

Pendekatan ini:

  1. Membaca seluruh file ke browser memory pake arrayBuffer()
  2. Bikin Blob dari in-memory data itu
  3. Bikin File object baru dari Blob (bukan dari rujukan file system asli)
  4. Memastikan file data tetap ada terlepas dari perubahan source file

Memory cost-nya ga seberapa buat gambar biasa (beberapa MB), dan sepenuhnya menghilangkan masalah file reference.

Baca Juga: Convert File HEIC ke JPG di React File Upload

Kapan Lo Bakal Ketemu Error Ini

Berdasarkan pengalaman dan riset gue, lo paling mungkin ketemu error ini waktu:

Production Infrastructure Patterns

  • Base paths di production: Pake basePath di next.config.js buat deployment di bawah subpath
  • Reverse proxy setups: Nginx, Apache, atau cloud load balancers di depan Next.js app lo
  • Multiple containers: Rolling deployments, blue-green deployments, atau multi-instance setups
  • CDN routing: CloudFlare, Fastly, atau CDN layers lain yang bisa memicu redirects

User Interaction Patterns

  • Copy-paste dari desktop: Users meng-copy files dari Finder/Explorer dan paste ke dropzone lo
  • Long form fills: Waktu yang cukup lama antara file selection dan form submission
  • Network interruptions: Koneksi lambat atau retry scenarios
  • Mobile Safari quirks: iOS file handling punya perilaku unik sendiri

Testing Fix Lo

Setelah menerapkan fixes ini, test secara menyeluruh dengan scenarios ini:

Local Testing

# Simulate production build locally
pnpm build
pnpm start

# Test dengan basePath kalo lo pake
NEXT_PUBLIC_APP_BASE_PATH=/your-path pnpm start

Production-Like Testing

  1. Copy-paste test: Copy image dari file manager lo, paste ke dropzone, wait 30 seconds, submit
  2. Screenshot test: Take screenshot, paste immediately, submit
  3. Drag-drop test: Drag multiple files dari desktop, submit setelah form filling
  4. Network simulation: Pake Chrome DevTools buat simulate slow 3G, test uploads dengan delays

Monitoring di Production

Add logging buat track upload failures:

try {
    const response = await submitWithRedirectSupport(url);
    // Log success metrics
    analytics.track("file_upload_success", {
        fileSize: file.size,
        fileType: file.type,
        duration: Date.now() - startTime,
    });
} catch (error) {
    // Log failure details
    analytics.track("file_upload_failed", {
        error: error.message,
        fileSize: file.size,
        userAgent: navigator.userAgent,
    });
    throw error;
}

Performance Considerations

Converting files ke in-memory blobs memang menambah langkah processing kecil, tapi dampaknya minimal:

  • Memory usage: File size × 2 saat conversion (sementara), terus balik ke 1×
  • Processing time: Sekitar 50-200ms buat gambar biasa (1-5 MB)
  • User experience: Terjadi langsung pas paste/drop, terasa instan buat users

Trade-off-nya sangat worth it buat reliability. Users lebih suka nunggu 100ms buat processing daripada ngalamin upload failures secara acak.

Baca Juga: Server Actions vs Client Rendering di Next.js: Panduan Developer 2025

Lessons Learned

Perjalanan debugging ini ngajarin gue beberapa pelajaran berharga:

  1. Production environments secara fundamental berbeda: Jangan pernah asumsikan perilaku localhost bisa diterjemahkan ke production
  2. Browser security itu kompleks: File object references lebih rumit dari yang keliatan
  3. Infrastructure itu penting: Setiap proxy, load balancer, dan redirect layer menambah kompleksitas
  4. Test dengan pola user asli: Copy-pasting dari desktop bukan sama dengan drag-drop
  5. Memory itu murah, reliability itu tak ternilai: Biaya memory kecil dari blob conversion sangat worth it buat stabilitas

Wrapping Up

Error ERR_UPLOAD_FILE_CHANGED itu salah satu production bugs yang bisa bikin lo mempertanyakan kewarasan lo. Jalan di localhost, harusnya jalan di production, tapi ternyata enggak. Memahami perilaku browser dan mekanisme file reference adalah kunci buat menyelesaikannya secara permanen.

Two-pronged fix ini, menstabilkan FormData across retries dan mengonversi file references ke in-memory blobs, menangani kedua infrastructure-based dan user-interaction-based failure modes. Implement keduanya, dan lo bakal punya file uploads yang solid yang jalan konsisten di semua environments dan usage patterns.

Kalo lo masih ngalamin issues setelah menerapkan fixes ini, check production logs lo buat pola error spesifik dan jangan ragu buat reach out. Kadang ada keanehan spesifik environment yang butuh solusi custom.

Happy debugging, dan semoga files lo selalu upload dengan sukses!

Back to Blog

Related Posts

View All Posts »