Ask Anvil

Answers to questions about automating PDFs, e-signatures, Webforms, and other paperwork problems.
Webforms
Categories

Why does my file upload arrive empty when the web form submits, and how do I fix it?

The symptom is specific. Every text field in the submission arrives intact, but the file field is empty. No error, no rejected request, just no bytes. There are two common causes, and they are easy to tell apart.

Cause 1: the form is not multipart

A form's encoding is application/x-www-form-urlencoded by default, and file content cannot be put inside URL parameters. So a file input needs two things: a POST method, because the content has nowhere to go in a URL, and a multipart encoding, because the data has to be split into a part per file.

<form action="/upload" method="post" enctype="multipart/form-data">
  <input type="file" name="contract" />
  <button type="submit">Upload</button>
</form>

Cause 2: you set Content-Type yourself

If you submit with fetch, the request needs a boundary string so the server can tell where each part starts and ends. fetch generates that boundary when you hand it a FormData body. Setting the header by hand overrides the generated one, and a literal "multipart/form-data" carries no boundary, so the parser has nothing to split on. Busboy 1.6.0 throws "Multipart: Boundary not found" rather than hand you a file.

const body = new FormData(document.querySelector('form'))

// Wrong: this header has no boundary, so the parser has nothing to split on
await fetch('/upload', {
  method: 'POST',
  body,
  headers: { 'Content-Type': 'multipart/form-data' },
})

// Right: omit the header and let fetch generate it
await fetch('/upload', { method: 'POST', body })

You can watch the header change in Node (this ran on v22), without a server running:

const fd = new FormData()
fd.set('contract', new Blob(['hello']), 'contract.pdf')

const auto = new Request('https://example.com/upload', { method: 'POST', body: fd })
const manual = new Request('https://example.com/upload', {
  method: 'POST',
  body: fd,
  headers: { 'Content-Type': 'multipart/form-data' },
})

console.log(auto.headers.get('content-type'))
// multipart/form-data; boundary=----formdata-undici-<random>
console.log(manual.headers.get('content-type'))
// multipart/form-data

Two things to check on the server

A JSON or urlencoded body parser will not read a multipart body. The body-parser docs say it outright: "This does not handle multipart bodies, due to their complex and typically large nature," and point you at busboy, multiparty, formidable, or multer instead. Each parser "only looks at requests where the Content-Type header matches the type option," so a multipart request passes straight through express.json() untouched.

Then check the request size limit before you rewrite anything. Nginx's client_max_body_size defaults to 1m and returns 413 past that, and your hosting platform likely has a cap of its own. A file that is simply too large fails in a different place than a file that was never encoded, and it is worth ruling out first.

Back to All Questions

The fastest way to build software for documents

Anvil Document SDK is a comprehensive toolbox for product teams launching document flows where PDF filling, signing, and complex conditional scenarios are necessary.
Explore Anvil
Anvil Webforms