javascriptroom blog

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.

2026-02

Table of Contents#

  1. Prerequisites
  2. Frontend Setup: Using Fetch and FormData
  3. Rails Backend Setup
  4. Common Issues: Why Rails Isn’t Receiving All Files
  5. Step-by-Step Troubleshooting Guide
  6. Best Practices for Reliable File Uploads
  7. Conclusion
  8. References

Prerequisites#

Before we start, ensure you have:

  • Basic knowledge of HTML, JavaScript, and Ruby on Rails.
  • A Rails 6+ application set up (we’ll use Rails 7 in examples).
  • A modern browser (Fetch and FormData are supported in all modern browsers).
  • Active Storage enabled in your Rails app (for handling file storage; we’ll cover setup briefly).

Frontend Setup: Using Fetch and FormData#

Let’s start by building the frontend. We’ll create a simple file input, capture selected files, and send them to Rails using Fetch and FormData.

Step 1: HTML File Input#

Add an HTML input element with the multiple attribute to allow selecting multiple files:

<!-- app/views/uploads/new.html.erb -->
<h1>Upload Multiple Files</h1>
<input type="file" id="file-upload" multiple accept="image/*"> <!-- "multiple" allows selecting many files -->
<button id="upload-btn">Upload Files</button>
<div id="status"></div>

Step 2: JavaScript to Handle Uploads#

Next, write JavaScript to listen for file selection, construct FormData, and send files via Fetch.

// app/javascript/packs/upload.js
document.addEventListener('DOMContentLoaded', () => {
  const fileInput = document.getElementById('file-upload');
  const uploadBtn = document.getElementById('upload-btn');
  const statusDiv = document.getElementById('status');
 
  uploadBtn.addEventListener('click', async () => {
    const files = fileInput.files; // FileList object containing selected files
    if (files.length === 0) {
      statusDiv.textContent = 'No files selected!';
      return;
    }
 
    // Create FormData object to hold file data
    const formData = new FormData();
 
    // Append each file to FormData with the key "files[]" (Rails expects array-like params)
    for (let i = 0; i < files.length; i++) {
      formData.append('files[]', files[i], files[i].name); // Key: "files[]", Value: File object
    }
 
    try {
      const response = await fetch('/uploads', {
        method: 'POST',
        body: formData, // FormData automatically sets correct Content-Type
        headers: {
          'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content // Required for Rails
        }
      });
 
      if (response.ok) {
        statusDiv.textContent = 'Files uploaded successfully!';
      } else {
        statusDiv.textContent = `Error: ${response.statusText}`;
      }
    } catch (error) {
      statusDiv.textContent = `Network error: ${error.message}`;
    }
  });
});

Key Notes for Frontend:#

  • 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.

Rails Backend Setup#

Now, set up the Rails backend to receive and process the files.

Step 1: Generate a Controller and Route#

Run rails generate controller Uploads new create and update config/routes.rb:

# config/routes.rb
Rails.application.routes.draw do
  get 'uploads/new', to: 'uploads#new', as: :new_upload
  post 'uploads', to: 'uploads#create', as: :uploads
end

Active Storage simplifies file storage in Rails. Run:

rails active_storage:install
rails db:migrate

This creates tables for storing file metadata.

Step 3: Controller Action to Handle Uploads#

Update app/controllers/uploads_controller.rb to process the uploaded files:

# app/controllers/uploads_controller.rb
class 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
  end
end

Key Notes for Backend:#

  • Strong Parameters: Use permit(files: []) to allow an array of files. If you use permit(:files), Rails will only accept a single file.
  • Accessing Files: params[:files] will be an array of ActionDispatch::Http::UploadedFile objects (Rails’ wrapper for uploaded files).

Common Issues: Why Rails Isn’t Receiving All Files#

Even with the above setup, you might find Rails only receives some files (or none). Let’s troubleshoot the most likely culprits.

1. Incorrect FormData Appending#

Problem: Forgetting to loop through files and append them with files[].

Example Mistake:

// ❌ Wrong: Appending the entire FileList instead of individual files
formData.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); 
}

2. Manually Setting the Content-Type Header#

Problem: Explicitly setting Content-Type: 'multipart/form-data' in Fetch headers.

Example Mistake:

// ❌ Wrong: Overrides Fetch's automatic Content-Type (lacks boundary)
headers: {
  'Content-Type': 'multipart/form-data', // Missing boundary!
  'X-CSRF-Token': ...
}

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.

Fix: Omit the Content-Type header entirely.

3. Misconfigured Strong Parameters#

Problem: Not permitting files as an array in Rails.

Example Mistake:

# ❌ Wrong: Permits a single file, not an array
def upload_params
  params.permit(:files) # Rails ignores all but the first file
end

Fix: Permit files as an array:

# ✅ Correct: Permits an array of files
def upload_params
  params.permit(files: []) 
end

4. File Size Limits#

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:
    # /etc/nginx/sites-available/your-app
    server {
      client_max_body_size 500M; # Allow 500MB uploads
      # ... other 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.

5. Browser/OS File Selection Limits#

Problem: Some browsers or OSes limit the number of files you can select in one go (rare but possible).

Fix: Test with a small number of files (e.g., 2–3) first. If that works, gradually increase the count.

Step-by-Step Troubleshooting Guide#

If files still aren’t reaching Rails, follow this workflow:

1. Inspect the Request in Browser DevTools#

  • Open Chrome DevTools → Network tab → Select the /uploads POST request.

  • Go to the Payload tab. Under "Form Data", verify all files are listed with the key files[].

    DevTools Form Data Screenshot
    Example: All files should appear here. If missing, the frontend isn’t sending them.

2. Check Rails Logs#

  • 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.

3. Debug Strong Parameters#

  • Add byebug (or binding.pry) in the create action to inspect params:
    def create
      byebug # Inspect params here
      uploaded_files = params[:files]
      # ...
    end
    In the debugger, run params[:files] to see if all files are present. If not, check upload_params.

4. Test with a Minimal Example#

  • 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).

Best Practices for Reliable File Uploads#

To avoid future issues:

  • 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.

Conclusion#

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.

References#