Upload Multiple Files with Fetch and FormData APIs: Troubleshooting Rails Server Not Receiving All Files
File uploads are a cornerstone of modern web applications, from photo galleries to document management systems. When building such features, developers often turn to the Fetch API (for making network requests) and FormData API (for constructing multipart form data) for their simplicity and native browser support. However, integrating these frontend tools with a Rails backend can sometimes lead to a frustrating issue: the Rails server fails to receive all uploaded files, even when the frontend seems to send them.
In this blog, we’ll demystify the process of uploading multiple files with Fetch and FormData, then dive deep into troubleshooting why Rails might not receive all files. We’ll cover common pitfalls, debugging techniques, and actionable fixes to ensure your file uploads work reliably.
FormData Appending: Always loop through files and append each file with the key files[] (the [] tells Rails to treat this as an array). If you skip the loop and append the entire FileList, Rails won’t receive the files.
CSRF Token: Rails requires the X-CSRF-Token header for non-GET requests. Include it using the meta tag from your layout.
No Manual Content-Type: Do NOT set Content-Type: 'multipart/form-data' in the headers. Fetch automatically generates this header with the required boundary (a unique string separating form data parts), which is critical for Rails to parse the request.
Run rails generate controller Uploads new create and update config/routes.rb:
# config/routes.rbRails.application.routes.draw do get 'uploads/new', to: 'uploads#new', as: :new_upload post 'uploads', to: 'uploads#create', as: :uploadsend
Step 2: Configure Active Storage (Optional but Recommended)#
Active Storage simplifies file storage in Rails. Run:
Update app/controllers/uploads_controller.rb to process the uploaded files:
# app/controllers/uploads_controller.rbclass UploadsController < ApplicationController def new end def create # Access uploaded files via params[:files] (an array) uploaded_files = params[:files] if uploaded_files.nil? || uploaded_files.empty? redirect_to new_upload_path, alert: 'No files received.' return end # Save each file using Active Storage (adjust as needed for your model) uploaded_files.each do |file| # Example: Attach to a hypothetical "Upload" model (create one with `rails generate model Upload`) Upload.create!(file: file) end redirect_to new_upload_path, notice: "#{uploaded_files.size} files uploaded successfully!" end private # Permit the "files" parameter (array) def upload_params params.permit(files: []) # Critical: Permit "files" as an array endend
Problem: Forgetting to loop through files and append them with files[].
Example Mistake:
// ❌ Wrong: Appending the entire FileList instead of individual filesformData.append('files', files); // Rails receives nothing or a single "FileList" object
Fix: Loop through files and append each with files[]:
// ✅ Correct: Append each file with the key "files[]"for (let i = 0; i < files.length; i++) { formData.append('files[]', files[i], files[i].name); }
Why It Fails: multipart/form-data requires a boundary string (e.g., boundary=----WebKitFormBoundaryabc123) to separate parts of the form data. Fetch generates this automatically when body: formData is used. Manually setting the header omits the boundary, so Rails can’t parse the request and files are lost.
Problem: Your web server (e.g., Nginx, Apache) or Rack middleware rejects large files, causing partial uploads. Rails itself does not enforce a specific upload size limit; the actual limit depends on your server configuration and any size constraints you explicitly set.
How to Check:
Web Server: Check your server's upload size limit. For Nginx, this is controlled by client_max_body_size (default is often 1MB). Update it in your Nginx config:
Rack Middleware: You can add size validation via Rack middleware (e.g., using Rack::Attack or a custom middleware) to limit upload sizes at the application level.
Rails: You can enforce file size limits in your controller or via custom validation in your models when using Active Storage.
Run rails server and watch the logs when uploading. Look for:
Started POST "/uploads" for ::1 at 2024-03-20 12:34:56 +0000
Processing by UploadsController#create as */*
Parameters: {"files"=>[#<ActionDispatch::Http::UploadedFile:0x0000...>, #<ActionDispatch::Http::UploadedFile:0x0000...>]}
If files is missing or has fewer entries than expected, the issue is in the frontend.
Strip down the frontend to a single file upload (remove multiple) and see if it works. If yes, the issue is likely with multi-file handling (e.g., FormData looping).
Validate File Types/Sizes Frontend: Use the accept attribute on the input (e.g., accept="image/*") and check file.size in JavaScript to reject oversized files early.
Handle Errors Gracefully: Use Fetch’s response.ok and Rails flash messages to inform users of success/failure.
Test Across Browsers: Ensure compatibility with Chrome, Firefox, and Safari (FormData behavior is consistent, but edge cases exist).
Monitor Server Logs: Track upload failures with tools like Sentry to catch issues in production.
Uploading multiple files with Fetch and FormData is powerful, but missteps in frontend FormData handling or Rails backend configuration can prevent files from being received. By following the setup steps, avoiding common pitfalls (like manual Content-Type headers or incorrect strong parameters), and using the troubleshooting guide, you’ll ensure reliable file uploads in your Rails app.