htmx send files using htmx.ajax() call
By Flavio Copes
Learn how to send files with an htmx.ajax() call by setting hx-encoding to multipart/form-data and passing the source element, in htmx 2 and htmx 4.
In a drag and drop file upload workflow I had the need to use htmx.ajax() to fire a network call to upload files, but I had some trouble sending data to the server.
Some context first. A file upload over HTTP needs the multipart/form-data encoding, because the default form encoding cannot carry binary file content. In a normal form, htmx handles this without JavaScript: you set enctype="multipart/form-data" on the form, add a file input, and hx-post does the rest.
Drag and drop is different. The files never pass through a form submission. My drop handler collected them into a FormData object, and I wanted to hand them to htmx programmatically with htmx.ajax(). The request fired, but the files never reached the server.
After some research I found out using htmx.ajax() you cannot (at the time of writing) set the Content-Type header to multipart/form-data, which is what makes file uploads possible in HTTP.
To overcome this, you must set the hx-encoding attribute in the HTML, and use the source property in the context object passed to htmx.ajax():
<div
id="dropzone"
hx-post="/upload"
hx-encoding="multipart/form-data"
hx-trigger="none"
>
hx-trigger="none" means the element never fires a request on its own. Only my drop handler does, with this call:
htmx.ajax('POST',
event.currentTarget.getAttribute('hx-post'), {
values: {
files: formData.getAll('files')
},
source: event.currentTarget,
})
The source property is the missing piece. It tells htmx which element the request belongs to, so the request picks up that element’s attributes, including hx-encoding="multipart/form-data". With the encoding in place, the File objects inside values are sent as real multipart parts, and the server receives them like any other upload.
To verify it worked, open the request in the Network panel. The Content-Type header must read multipart/form-data with a boundary string, and the payload view shows each file as its own part.
If you see application/x-www-form-urlencoded instead, htmx did not pick up the encoding. Check that source points at the element carrying hx-encoding.
Everything above is how it works in htmx 2.
What changes in htmx 4
htmx 4 sends requests with fetch() instead of XMLHttpRequest. Uploads still need hx-encoding="multipart/form-data" on the source element. Without it, htmx 4 URL-encodes the values, and files can’t travel that way.
The values object is where the code above breaks. htmx 4 sends an array in values as a single text value, so files: formData.getAll('files') reaches the server as the string [object File],[object File] instead of two files. A single File in values still goes out as a file.
To send several dropped files, put a hidden file input inside a form, copy the dropped files into it, and pass the form as source:
<form
id="dropzone"
hx-post="/upload"
hx-encoding="multipart/form-data"
hx-trigger="none"
>
<input type="file" name="files" multiple hidden>
</form>
The drop handler copies the files into the input and fires the request:
const form = event.currentTarget
form.querySelector('input[name="files"]').files = event.dataTransfer.files
htmx.ajax('POST', form.getAttribute('hx-post'), {
source: form,
})
htmx adds the fields of the source form to the request, the file input included, so each file arrives as its own multipart part.
The htmx 4 reference pages for htmx.ajax() and hx-encoding have the details.
Want me to talk about your product? You can sponsor this site.