Triggers, forms, and feedback
Upload a file
Use multipart form encoding and a real file input so the server receives binary data with the other form values.
File content cannot travel in the default form encoding. Uploads need multipart/form-data, the HTTP encoding that carries binary parts alongside ordinary fields. A real upload form declares it explicitly:
<form action="/attachments" method="post"
enctype="multipart/form-data"
hx-post="/attachments"
hx-target="#upload-result">
<label>
Attachment
<input type="file" name="attachment" accept="image/*" required>
</label>
<button>Upload</button>
</form>
<div id="upload-result"></div>
Without JavaScript, the browser posts the file to /attachments and navigates. With HTMX loaded, the same submission happens in the background and the server’s response lands in #upload-result. The file arrives under the field name attachment, next to any other named controls in the form.
You can alternatively set hx-encoding="multipart/form-data" on the HTMX request context. Keep the native enctype when the form must work without JavaScript.
The hx-encoding attribute becomes essential when the upload does not flow through a form at all. In a drag-and-drop workflow, your code collects files and starts the request with htmx.ajax(), which cannot set the multipart content type by itself. Put hx-encoding on the element and pass it as the source:
htmx.ajax('POST', '/attachments', {
values: { attachment: formData.getAll('attachment') },
source: dropzone,
})
The request inherits the source element’s attributes, including the encoding, so the files are sent as real multipart parts. Confirm it in the Network panel: the request’s Content-Type must be multipart/form-data with a boundary, not application/x-www-form-urlencoded.
HTMX uses XMLHttpRequest, so htmx:xhr:progress can drive a progress display for large uploads. Treat it as feedback, not proof that the server accepted or stored the file.
The server carries the real safety duties. It must enforce a size limit, inspect actual content rather than trusting the filename or MIME declaration, generate a safe storage name, authorize who may attach the file, and keep uploads outside executable paths. accept="image/*" only filters the file chooser; it validates nothing, and anyone can send any bytes to your endpoint directly.
Test an oversized file, a renamed non-image, a lost connection, and a valid upload. Every path needs a recoverable response the person can act on.
Lesson completed