Development

Fixing ERR_UPLOAD_FILE_CHANGED in Next.js Production

Asep Alazhari

Solve the mysterious file upload error that only happens in production. Learn why FormData rebuilds and file references cause upload failures in Next.js apps.

Fixing ERR_UPLOAD_FILE_CHANGED in Next.js Production

The Mystery of the Vanishing Files

You’ve just deployed your Next.js application to production, and everything looks perfect. Your file upload feature works flawlessly on localhost, users can drag and drop images, paste screenshots, even copy files from their desktop. You’ve tested it dozens of times. You go to bed feeling accomplished.

Then the messages start rolling in. “I can’t upload files.” “The submit button doesn’t work.” “It keeps failing with a weird error.” You check the browser console and there it is, mocking you in red: net::ERR_UPLOAD_FILE_CHANGED.

ERR_UPLOAD_FILE_CHANGED error in browser console

Browser Network tab showing the ERR_UPLOAD_FILE_CHANGED error that only appears in production environments - a frustrating issue that can break file upload functionality

But here’s the kicker, you try it yourself on localhost, and it works perfectly. Again. And again. The error only appears in production, and it’s driving you mad.

I’ve been there. Last week, I spent hours debugging this exact issue in a budget management system I was building. The file upload worked beautifully in development, but users in production were experiencing random failures when submitting activity forms with evidence files. Some uploads succeeded, others failed mysteriously. The pattern was frustratingly inconsistent.

After diving deep into browser behavior, FormData lifecycle, and Next.js production configurations, I discovered something fascinating: this isn’t just a bug, it’s a perfect storm of browser security, file system references, and production infrastructure that doesn’t exist in development.

If you’re reading this because you’re facing the same nightmare, you’re in the right place. Let’s solve this together.

Understanding the Root Cause

The Two-Faced File Object

The ERR_UPLOAD_FILE_CHANGED error occurs when the browser detects that a File object’s contents have changed between the time it was selected and when it’s actually uploaded. But why would a file change? You’re not modifying it, right?

Here’s the twist: it’s not about the file changing, it’s about how the File object references the data.

When you handle file uploads in JavaScript, you’re working with File objects that can reference data in two fundamentally different ways:

  1. File System Reference: Points to a file on disk (used when you copy-paste from Finder/Explorer)
  2. In-Memory Data: Contains actual bytes in browser memory (used for screenshots and some drag-drop operations)

This difference is invisible in development but becomes critical in production environments with complex infrastructure.

Why Production Is Different

Your localhost setup is simple: browser → Next.js dev server. One hop, no redirects, no proxy layers.

Production is a different beast entirely:

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

In my case, the production environment used a /busdev base path, meaning every API request went through multiple redirect layers:

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

Each redirect in this chain created an opportunity for File references to break. When FormData was rebuilt during redirect handling, the browser detected that File object references had changed and threw ERR_UPLOAD_FILE_CHANGED.

The Problem Pattern

Scenario 1: The FormData Rebuild Issue

Here’s what was happening in my code (and possibly yours):

// BROKEN: FormData rebuilt on every 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 each time!
            credentials: "include",
            redirect: "manual",
        });

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

        return response;
    }
};

The problem? Every time we hit a redirect and retry, buildFormData() creates a brand new FormData instance with fresh File object references. The browser’s security mechanism detects this and assumes the files have been tampered with.

Scenario 2: The Pasted File Reference Issue

But wait, there’s more! Even after fixing the FormData rebuild issue, I discovered another problem: pasting files from Finder/Explorer.

When users paste images two different ways:

  • Screenshot (Cmd/Ctrl + Shift + 4, then paste): Works fine
  • Copy file from Finder (Cmd/Ctrl + C, then paste): Fails with ERR_UPLOAD_FILE_CHANGED

Why? Because clipboardData.items[i].getAsFile() returns different types of File objects:

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

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

    // File gets stored in state...
    setUploadedFile(file);

    // ...but by upload time, the reference might be invalid!
};

For copied files, the File object points to the source file location. If that file moves, gets deleted, or the OS invalidates the reference by the time you click submit, boom, ERR_UPLOAD_FILE_CHANGED.

The Complete Solution

Fix 1: Stabilize FormData Across Retries

The first fix is straightforward: build FormData once before the retry loop and reuse the same instance:

// FIXED: FormData built once and reused
const submitWithRedirectSupport = async (url: string) => {
    let currentUrl = url;
    const formData = buildFormData(); // Build ONCE outside loop

    for (let attempt = 0; attempt < 3; attempt += 1) {
        // Add exponential backoff for 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 same FormData instance
                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");
};

This ensures File object references remain consistent across all retry attempts, eliminating the redirect-based error.

Fix 2: Convert File References to In-Memory Blobs

The second fix handles the paste-from-Finder scenario by immediately reading file data into memory:

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

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

        // Now the file data is guaranteed to be in memory
        stabilizedFile.preview = URL.createObjectURL(stabilizedFile);
        onFileUpload(fileType, stabilizedFile);
    },
    [fileType, onFileUpload]
);

This approach:

  1. Reads the entire file into browser memory using arrayBuffer()
  2. Creates a Blob from that in-memory data
  3. Creates a new File object from the Blob (not the original file system reference)
  4. Ensures file data persists regardless of source file changes

The memory cost is negligible for typical images (a few MB), and it completely eliminates file reference issues.

Also Read: Enhancing File Uploads with HEIC to JPG Conversion in React

When You’ll Encounter This

Based on my experience and research, you’re most likely to hit this error when:

Production Infrastructure Patterns

  • Base paths in production: Using basePath in next.config.js for deployment under a subpath
  • Reverse proxy setups: Nginx, Apache, or cloud load balancers in front of your Next.js app
  • Multiple containers: Rolling deployments, blue-green deployments, or multi-instance setups
  • CDN routing: CloudFlare, Fastly, or other CDN layers that can trigger redirects

User Interaction Patterns

  • Copy-paste from desktop: Users copying files from Finder/Explorer and pasting into your dropzone
  • Long form fills: Significant time between file selection and form submission
  • Network interruptions: Slow connections or retry scenarios
  • Mobile Safari quirks: iOS file handling has its own unique behavior

Testing Your Fix

After implementing these fixes, test thoroughly with these scenarios:

Local Testing

# Simulate production build locally
pnpm build
pnpm start

# Test with basePath if you use one
NEXT_PUBLIC_APP_BASE_PATH=/your-path pnpm start

Production-Like Testing

  1. Copy-paste test: Copy an image from your file manager, paste into dropzone, wait 30 seconds, submit
  2. Screenshot test: Take a screenshot, paste immediately, submit
  3. Drag-drop test: Drag multiple files from desktop, submit after form filling
  4. Network simulation: Use Chrome DevTools to simulate slow 3G, test uploads with delays

Monitoring in Production

Add logging to 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 to in-memory blobs does add a small processing step, but the impact is minimal:

  • Memory usage: File size × 2 during conversion (temporary), then back to 1×
  • Processing time: ~50-200ms for typical images (1-5 MB)
  • User experience: Happens immediately on paste/drop, feels instant to users

The trade-off is absolutely worth it for reliability. Users would rather wait 100ms for processing than experience random upload failures.

Also Read: Server Actions vs Client Rendering in Next.js: The 2025 Guide

Lessons Learned

This debugging journey taught me several valuable lessons:

  1. Production environments are fundamentally different: Never assume localhost behavior translates to production
  2. Browser security is nuanced: File object references are more complex than they appear
  3. Infrastructure matters: Every proxy, load balancer, and redirect layer adds complexity
  4. Test with real user patterns: Copy-pasting from desktop isn’t the same as drag-drop
  5. Memory is cheap, reliability is priceless: The small memory cost of blob conversion is worth the stability

Wrapping Up

The ERR_UPLOAD_FILE_CHANGED error is one of those production bugs that can make you question your sanity. It works locally, it should work in production, but it doesn’t. Understanding the underlying browser behavior and file reference mechanics is key to solving it permanently.

The two-pronged fix, stabilizing FormData across retries and converting file references to in-memory blobs, addresses both the infrastructure-based and user-interaction-based failure modes. Implement both, and you’ll have rock-solid file uploads that work consistently across all environments and usage patterns.

If you’re still experiencing issues after applying these fixes, check your production logs for specific error patterns and feel free to reach out. Sometimes there are environment-specific quirks that need custom solutions.

Happy debugging, and may your files always upload successfully!

Back to Blog

Related Posts

View All Posts »