Skip to main content

Upload Files via Multipart/Form-Data in Salesforce LWC: Fetch API & Boundary Guide

In plain words: Multipart/form-data upload in Lightning Web Components (LWC) is a browser-standard technique used to send binary files (like PDFs, images, or spreadsheets) together with metadata fields in a single HTTP request. In modern JavaScript, the browser automatically builds the multipart payload and generates the unique boundary delimiter when you append file objects to a standard FormData instance.

Enterprise applications often require streaming files directly from the user's browser to third-party endpoints—such as Amazon S3, Azure Blob Storage, Box, or external OCR microservices. Rather than routing large files through Apex and consuming restrictive heap size limits, you can construct and dispatch multipart uploads directly from an LWC component using the standard FormData and fetch() APIs.

1. How Multipart Form Data & Boundaries Work

When sending files over HTTP, the request needs a mechanism to separate text metadata from raw binary file streams:

  • The Boundary Delimiter: A unique string that marks the start and end of every field and file part in the request body.
  • The FormData Interface: A native browser utility that collects key-value pairs and binary blobs.
  • Automatic Header Generation: When you pass a FormData object to fetch(), the browser automatically configures the Content-Type: multipart/form-data; boundary=... header, calculating exact byte boundaries on the fly.
360 Client-Side Multipart Upload Card:
  • Payload Container: Native JavaScript new FormData().
  • HTTP Client: Modern fetch(url, options) with async/await.
  • Salesforce Network Rule: External domain must be whitelisted in Setup > CSP Trusted Sites (connect-src).
  • UX Feedback: Progress tracking via <lightning-spinner> and alerts via ShowToastEvent.

2. Step-by-Step Implementation

Below is a complete, production-ready component that accepts local files, validates attachments, packages the data into a FormData container, and sends it via fetch().

Step 1: Build the Template Markup (fileUploaderMultipart.html)
<template>
    <lightning-card title="Direct Multipart File Upload" icon-name="doctype:attachment">
        <div class="slds-p-around_medium">
            <!-- Loading Spinner -->
            <template lwc:if={isUploading}>
                <lightning-spinner alternative-text="Uploading file..." size="small"></lightning-spinner>
            </template>

            <lightning-input
                type="file"
                label="Select Document"
                accept=".pdf,.png,.jpg,.jpeg,.docx"
                onchange={handleFileSelection}>
            </lightning-input>

            <template lwc:if={selectedFileName}>
                <div class="slds-box slds-theme_shade slds-m-vertical_small">
                    <p>Selected File: <strong>{selectedFileName}</strong> ({selectedFileSize})</p>
                </div>
            </template>

            <lightning-button
                label="Upload File"
                variant="brand"
                icon-name="utility:upload"
                disabled={isUploadDisabled}
                onclick={handleFileUpload}>
            </lightning-button>
        </div>
    </lightning-card>
</template>
Step 2: Implement the JavaScript Controller (fileUploaderMultipart.js)
import { LightningElement } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';

export default class FileUploaderMultipart extends LightningElement {
    fileToUpload = null;
    selectedFileName = '';
    selectedFileSize = '';
    isUploading = false;

    get isUploadDisabled() {
        return !this.fileToUpload || this.isUploading;
    }

    handleFileSelection(event) {
        const files = event.target.files;
        if (files && files.length > 0) {
            this.fileToUpload = files[0];
            this.selectedFileName = this.fileToUpload.name;
            
            // Format file size
            const sizeInKb = (this.fileToUpload.size / 1024).toFixed(2);
            this.selectedFileSize = `${sizeInKb} KB`;
        }
    }

    async handleFileUpload() {
        if (!this.fileToUpload) {
            return;
        }

        this.isUploading = true;

        // 1. Build standard FormData payload
        const formData = new FormData();
        formData.append('file', this.fileToUpload, this.fileToUpload.name);
        formData.append('uploadedFrom', 'Salesforce_LWC');
        formData.append('timestamp', new Date().toISOString());

        const targetEndpoint = 'https://api.externalstorage.com/v1/upload';

        try {
            // 2. Dispatch request via Fetch API
            // NOTE: Do NOT manually set Content-Type header; the browser sets boundary automatically
            const response = await fetch(targetEndpoint, {
                method: 'POST',
                headers: {
                    'Accept': 'application/json'
                    // Add authorization headers if needed
                },
                body: formData
            });

            if (!response.ok) {
                throw new Error(`Upload failed with HTTP status ${response.status}: ${response.statusText}`);
            }

            const responseData = await response.json();
            console.log('Upload successful:', responseData);

            this.showToast('Success', 'File uploaded successfully via multipart form data.', 'success');
            this.resetForm();
        } catch (error) {
            console.error('File Upload Error:', error);
            this.showToast('Upload Error', error.message || 'Network error during file transfer.', 'error');
        } finally {
            this.isUploading = false;
        }
    }

    resetForm() {
        this.fileToUpload = null;
        this.selectedFileName = '';
        this.selectedFileSize = '';
    }

    showToast(title, message, variant) {
        this.dispatchEvent(
            new ShowToastEvent({
                title: title,
                message: message,
                variant: variant
            })
        );
    }
}
Step 3: Metadata Configuration (fileUploaderMultipart.js-meta.xml)
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>60.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__AppPage</target>
        <target>lightning__RecordPage</target>
        <target>lightning__HomePage</target>
    </targets>
</LightningComponentBundle>

3. Salesforce Security & Setup Requirements

To enable direct browser HTTP requests to external endpoints, you must configure network trust policies in Salesforce:

  • Add to CSP Trusted Sites: Navigate to Setup > Security > CSP Trusted Sites and register the destination base URL (e.g., https://api.externalstorage.com).
  • Enable connect-src: Check the connect-src directive on the CSP Trusted Site record to permit asynchronous Fetch and XHR network traffic.

4. Common Traps & Development Best Practices

Header Trap: Manually Setting the Content-Type Header on FormData
Manually setting headers: { 'Content-Type': 'multipart/form-data' } or adding a hardcoded boundary string breaks the request. The browser needs to generate its own internal boundary parameter to match the exact multipart format. When using FormData with fetch(), omit the Content-Type header completely so the browser assigns the correct header and matching boundary string automatically.
Legacy API Trap: Using Deprecated XMLHttpRequest Instead of fetch()
Writing raw XMLHttpRequest (XHR) boilerplate is outdated and error-prone compared to modern ECMAScript standards. Use the promise-based fetch() API with async/await for cleaner code readability, standardized error catching, and compatibility with Lightning Web Security (LWS).
Core Rule: Append files to a standard FormData instance, let the browser manage the boundary string automatically by omitting Content-Type, and execute the request using modern async/await Fetch syntax.
  • Client vs. Server-Side Routing: Direct browser uploads are ideal for non-sensitive public uploads or signed URL architectures (like AWS S3 Presigned URLs). If the upload requires secret API tokens, delegate the request to an Apex callout backed by Named Credentials.
  • File Size Validation: Always validate file.size on the client side before calling fetch() to reject oversized files before consuming network bandwidth.
  • Disable Action Controls: Toggle the upload button's disabled state and display a <lightning-spinner> during transit to prevent duplicate file submissions.

Summary

Uploading files using multipart/form-data in Lightning Web Components provides a modern, high-performance way to send documents directly from the user's browser to external endpoints. By combining the native FormData interface with the fetch() API and CSP whitelisting, developers can build scalable file uploaders while bypassing Apex heap limits entirely.