File Upload Not Working
You added file uploads to your app. Small files work. Large files don't. Or nothing works in production. Or uploads succeed but the file isn't where you expect it. File uploads break in specific, predictable ways — here's every common cause and fix.
Error: 413 Payload Too Large
This is the most common file upload error. Your server or a proxy in front of it is rejecting the file because it's too big.
There are multiple size limits that can trigger this, and you need to fix the right one:
Nginx limit (most common): Nginx defaults to a 1MB maximum body size. Even if your app accepts larger files, nginx rejects them before they reach your app.
# In nginx.conf or your site's server block:
client_max_body_size 50M; # Allow files up to 50MB
# You may also want to increase timeouts for large uploads:
proxy_read_timeout 300s;
proxy_send_timeout 300s;
Node.js/Express limit: If you're using body-parser or express.json(), they have their own limits.
// Express body parser defaults to 100KB for JSON
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ limit: '50mb', extended: true }));
PHP limit: PHP has multiple settings that affect upload size.
; In php.ini:
upload_max_filesize = 50M
post_max_size = 55M ; Must be larger than upload_max_filesize
memory_limit = 128M ; Must be larger than post_max_size
max_execution_time = 300 ; Give large uploads time to complete
Cloud platform limits: Vercel limits request bodies to 4.5MB in serverless functions. AWS API Gateway limits to 10MB. If you need larger uploads on these platforms, use pre-signed URLs to upload directly to S3/storage.
Error: File uploads silently fail
The upload appears to succeed but the file isn't saved. Common causes:
Wrong upload directory. Your code saves to ./uploads but that directory doesn't exist in production, or it resolves to a different path than you expect.
// Make sure the directory exists
const uploadDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
Permission denied. The upload directory exists but your app doesn't have write permission.
# Check ownership and permissions
ls -la /path/to/uploads/
# Fix it (make your app user the owner)
sudo chown -R www-data:www-data /path/to/uploads/
chmod 755 /path/to/uploads/
Ephemeral filesystem. If you're on a serverless platform (Vercel, AWS Lambda) or a containerized deployment, the filesystem is temporary. Files saved to disk disappear when the instance restarts. You need to upload to persistent storage like S3, GCS, or Supabase Storage.
Missing multipart middleware. File uploads use multipart/form-data encoding, which regular body parsers don't handle. You need middleware like multer (Express) or formidable.
// Express without multer — req.file is undefined
app.post('/upload', (req, res) => {
console.log(req.file); // undefined!
});
// Express with multer — req.file has the upload
const multer = require('multer');
const upload = multer({ dest: 'uploads/' });
app.post('/upload', upload.single('file'), (req, res) => {
console.log(req.file); // { filename, path, size, ... }
});
Error: Frontend form not sending the file
Your backend is fine but the file never arrives. The most common frontend mistake is not using the correct encoding.
<!-- WRONG — default encoding doesn't support files -->
<form action="/upload" method="POST">
<input type="file" name="file">
<button>Upload</button>
</form>
<!-- RIGHT — must specify enctype -->
<form action="/upload" method="POST" enctype="multipart/form-data">
<input type="file" name="file">
<button>Upload</button>
</form>
If you're using JavaScript to upload:
// WRONG — sending JSON with the file
fetch('/upload', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ file: fileInput.files[0] }) // doesn't work
});
// RIGHT — use FormData
const formData = new FormData();
formData.append('file', fileInput.files[0]);
fetch('/upload', {
method: 'POST',
body: formData
// Don't set Content-Type header — browser sets it automatically
// with the correct multipart boundary
});
The most common mistake with fetch and FormData: don't set the Content-Type header manually. The browser needs to set it automatically so it includes the multipart boundary string.
Large files: use direct-to-storage uploads
For files over a few megabytes, the best approach is to skip your server entirely and upload directly to cloud storage using pre-signed URLs.
- Client asks your server for a pre-signed upload URL
- Server generates a URL that's valid for a few minutes (using S3, GCS, or similar SDK)
- Client uploads the file directly to storage using that URL
- Client tells your server the upload is complete
This avoids all the size limits on your server, reduces server load, and handles large files reliably. It's more setup, but it's how production apps handle file uploads at scale.
Why AI gets file uploads wrong
AI generates the basic upload code but misses the infrastructure around it. It doesn't configure nginx's body size limit, doesn't handle the ephemeral filesystem problem, doesn't set the right encoding, and defaults to server-side storage instead of cloud storage. These are deployment concerns, not code concerns, and AI handles them at a code level when they need to be handled at an infrastructure level.
File uploads still broken?
MeatButton connects you with developers who can debug your upload pipeline end-to-end — from the browser form to nginx to your app to storage. First one's free.
Get MeatButton