skip to content

In Django, why is request.FILES empty after a user submits a form with a file input, and what must the request look like?

level: juniorimportance: must knowfreq 60%

answer

  1. how the browser encodes the form
  2. multipart/form-data
  3. only POST bodies are parsed
  4. keyed by the input's name

basics

~10 s

Django fills request.FILES only for a POST request whose form used enctype="multipart/form-data" and actually sent a file. Without that enctype the browser sends only the file name as text, so FILES stays empty.

solid answer

~30 s

`request.FILES` is populated only when three things hold: the method is `POST`, the `<form>` has `enctype="multipart/form-data"`, and at least one file field was actually sent. Without the enctype the browser URL-encodes the form and sends just the file name as a text value, which lands in `request.POST`, never in `FILES`. Django also parses request bodies into `POST` and `FILES` only for `POST`, so a `PUT` or `PATCH` upload leaves both empty. `request.FILES` is a `MultiValueDict` keyed by each input's `name` attribute, holding `UploadedFile` objects; use `request.FILES.getlist("scans")` when several files share one name. Forms need both dicts: `InvoiceForm(request.POST, request.FILES)`.

code

html · 5 lines
html
<form method="post" enctype="multipart/form-data">
  {% csrf_token %}
  <input type="file" name="invoice" accept="application/pdf,image/*">
  <button type="submit">Upload invoice</button>
</form>

go deeper

for a junior

Remember the three conditions: POST, enctype="multipart/form-data", and a named file input that was actually sent.

for a middle

Explain what the browser sends without the enctype, why only POST bodies are parsed, and how MultiValueDict indexing differs from getlist().

for a senior

Diagnose upload bugs from the request's Content-Type and method first, and keep upload views from reading request.body.

for a principal

Decide whether browser uploads go through Django forms, an API endpoint or direct-to-storage flows, and standardise the request shape across clients.

## What request.FILES is `HttpRequest.FILES` is a dictionary-like object (a `MultiValueDict`) that maps each file input's `name` attribute to one or more `UploadedFile` objects. It is built lazily: the first time the view, a form or middleware touches `request.POST` or `request.FILES`, Django parses the request body and fills both. The Django documentation states the conditions precisely: `FILES` only contains data if 1. the request method was `POST`, 2. at least one file field was actually posted, and 3. the `<form>` that posted the request had `enctype="multipart/form-data"`. Otherwise it is an empty dictionary-like object. Each condition corresponds to a common bug. ## Bug 1: the missing enctype An HTML form without an `enctype` attribute is sent as `application/x-www-form-urlencoded`. That encoding cannot carry binary content, so the browser sends only the **file name** as an ordinary text value. The result is confusing: - `request.POST["invoice"]` contains something like `"scan-0042.pdf"`; - `request.FILES` is empty; - a Django form bound with `request.FILES` reports the file field as required and missing. The fix is one attribute: `<form method="post" enctype="multipart/form-data">`. A form rendered from a Django form class exposes `form.is_multipart()`, which templates can use to decide whether to add it. ## Bug 2: not a POST Django's request parsing runs only for `POST`. For any other method, `request.POST` and `request.FILES` are empty, even if the body is valid multipart data. A JavaScript client that uploads with `PUT` or `PATCH` therefore sees nothing in `FILES`. Either send `POST`, or parse the body yourself; API frameworks built on Django provide parsers for this. ## Bug 3: the input name The key in `request.FILES` is the input's `name`, not its `id`: | Template | View access | |---|---| | `<input type="file" name="invoice">` | `request.FILES["invoice"]` | | `<input type="file" name="scans" multiple>` | `request.FILES.getlist("scans")` | | `<input type="file" id="invoice">` (no name) | nothing is submitted | `request.FILES["scans"]` on a multi-file input returns only the **last** file, because `MultiValueDict.__getitem__` returns the last value; `getlist()` returns all of them. ## Other things that are not the cause - **CSRF.** A missing `{% csrf_token %}` produces a 403 before the view runs, not an empty `FILES`. - **Size.** A large file is still delivered; size only decides whether Django keeps it in memory or spools it to a temporary file. - **Reading `request.body`.** It does not empty `FILES`, but it pulls the whole body into memory, raises `RequestDataTooBig` for bodies over 2.5 MB, and raises `RawPostDataException` if `FILES` was already parsed from the stream. Upload views should not read `request.body`. ## Passing files to a form A bound form needs both dictionaries: `InvoiceForm(request.POST, request.FILES)`. Passing only `request.POST` is another way to end up with "this field is required" on a file that was uploaded correctly. ## Testing an upload view Django's test client posts `multipart/form-data` by default, so a test exercises the same parsing path as a browser: - build a file with `SimpleUploadedFile("scan.pdf", b"%PDF-1.7 ...", content_type="application/pdf")`, or pass an open file object; - call `client.post(url, {"invoice": upload})`, using the input's name as the key; - assert on the outcome, for example that a record was created or that a missing file gives a 400. A test that posts with `content_type="application/x-www-form-urlencoded"` reproduces the missing-enctype bug, which makes it a useful regression test for templates that build forms by hand. ## Debugging checklist 1. Inspect the request in the browser's network panel: the `Content-Type` must start with `multipart/form-data; boundary=...`. 2. Confirm the method is `POST`. 3. Confirm the input has a `name`. 4. Print `request.FILES.keys()` in the view to see what arrived. 5. Check that the form is bound with `request.FILES`.

  • A JavaScript client uploads a scan with fetch() using PUT and multipart/form-data. Why is request.FILES empty?
    Django parses the body into `request.POST` and `request.FILES` only when the method is `POST`; for any other method both are empty regardless of content type. Switch the client to `POST`, or parse the body yourself, which API frameworks on top of Django do with their own parsers.
  • With <input type="file" name="scans" multiple>, why does request.FILES["scans"] return only one file?
    `request.FILES` is a `MultiValueDict`, and indexing returns the last value for a key. Use `request.FILES.getlist("scans")` to get every uploaded file under that name as a list of `UploadedFile` objects.

saying these in an interview costs you the question

  • Files larger than 2.5 MB never reach request.FILES
  • request.FILES is filled for PUT and PATCH multipart requests too
  • The key in request.FILES is the input's id attribute
  • A missing csrf_token is why request.FILES is empty
  • request.FILES["field"] returns all files for a multiple input