skip to content

In Django, how do you write a custom FileUploadHandler and install it for one view, and why must that happen before request.POST is accessed?

level: seniorimportance: nice to knowfreq 22%

answer

  1. subclass FileUploadHandler
  2. receive_data_chunk passes data on
  3. request.upload_handlers list
  4. parsing freezes the handler list
  5. CsrfViewMiddleware reads POST

basics

~20 s

Subclass FileUploadHandler, implement receive_data_chunk() and file_complete(), and insert an instance into request.upload_handlers at the start of the view. Parsing request.POST or request.FILES freezes the list, and CsrfViewMiddleware reads POST first, so the view needs csrf_exempt plus an inner csrf_protect.

solid answer

~40 s

A handler subclasses `django.core.files.uploadhandler.FileUploadHandler`. The parser calls `handle_raw_input()` once, `new_file()` per file, `receive_data_chunk(raw_data, start)` per chunk (return the data to pass it to the next handler, or `None` to consume it), `file_complete(file_size)` (return an `UploadedFile` to claim the file, or `None`), then `upload_complete()`; `upload_interrupted()` handles cleanup. Handlers can raise `StopUpload`, `SkipFile` or `StopFutureHandlers`. Install globally via `FILE_UPLOAD_HANDLERS`, or per view with `request.upload_handlers.insert(0, Handler(request))`. That must happen before `request.POST` or `request.FILES` is touched: parsing replaces the list with an immutable one and later changes raise `AttributeError`. Because `CsrfViewMiddleware` reads `request.POST` before the view, the documented pattern is `@csrf_exempt` on the outer view that installs the handler and `@csrf_protect` on an inner function that processes the request.

code

python · 20 lines
python
from django.http import HttpResponse
from django.shortcuts import redirect
from django.views.decorators.csrf import csrf_exempt, csrf_protect

from .uploadhandlers import MaxSizeUploadHandler


@csrf_exempt  # CsrfViewMiddleware would otherwise read request.POST first
def upload_invoice(request):
    request.upload_handlers.insert(0, MaxSizeUploadHandler(request))
    return _upload_invoice(request)


@csrf_protect  # the CSRF check still runs, now after the handler is installed
def _upload_invoice(request):
    if getattr(request, "upload_too_large", False):
        return HttpResponse("Invoice exceeds 20 MB", status=413)
    scan = request.FILES.get("scan")
    ...
    return redirect("invoices:list")

go deeper

for a junior

Know that upload handlers exist, that the defaults are set in FILE_UPLOAD_HANDLERS, and that a view can add its own through request.upload_handlers.

for a middle

Walk through the callbacks and their return values, the three control exceptions, and why the list must change before POST is parsed.

for a senior

Apply the csrf_exempt plus csrf_protect split correctly, handle StopUpload in the view, and understand that handlers see data before the CSRF check.

for a principal

Judge whether upload-time logic belongs in a custom handler, at the edge, or in direct-to-storage uploads that bypass Django workers.

## The handler protocol An upload handler is an object the multipart parser feeds as it reads the body. Subclass `django.core.files.uploadhandler.FileUploadHandler` (its default `chunk_size` is 64 KiB) and override the callbacks you need: | Callback | Called | Return value | |---|---|---| | `handle_raw_input(input_data, META, content_length, boundary, encoding=None)` | once, before parsing | a `(POST, FILES)` tuple to take over parsing, or `None` | | `new_file(field_name, file_name, content_type, content_length, charset=None, content_type_extra=None)` | when a file part starts | nothing; may raise `StopFutureHandlers` | | `receive_data_chunk(raw_data, start)` | for each chunk | the bytes for the next handler, or `None` to stop them | | `file_complete(file_size)` | when a file part ends | an `UploadedFile` to put in `request.FILES`, or `None` | | `upload_complete()` | after the whole body | nothing | | `upload_interrupted()` | if the upload was cut short | nothing; clean up | Three exceptions steer the parser: - `StopUpload(connection_reset=False)` stops the whole upload; with `connection_reset=True` Django stops reading immediately instead of consuming the rest of the body; - `SkipFile` drops the current file and moves on; - `StopFutureHandlers` claims the current file so later handlers are not consulted, which is how `MemoryFileUploadHandler` works. Client-supplied values such as `content_length` and `content_type` are not trustworthy; the base class docstring warns about exactly that. ## Installing a handler There are two scopes: 1. **Globally**, by editing `FILE_UPLOAD_HANDLERS` in settings, for behaviour every upload should get. 2. **Per request**, by modifying `request.upload_handlers` in the view. `insert(0, handler)` puts yours first so it sees every chunk before the defaults; assigning a new list replaces them. Per-request installation has a strict timing rule. `request.upload_handlers` can only be changed **before** `request.POST` or `request.FILES` is first accessed. When parsing starts, Django replaces the list with an `ImmutableList`, and afterwards assignment or insertion raises `AttributeError` ("You cannot set the upload handlers after the upload has been processed"). ## The CSRF complication `CsrfViewMiddleware`, enabled by default, reads `request.POST` in its `process_view()` to find the token, and that happens before your view runs. By the time the view body executes, parsing is already done. The Django documentation's pattern is therefore: 1. decorate the **outer** view with `csrf_exempt`, so the middleware skips it and does not touch `request.POST`; 2. install the handler in that outer view; 3. call an **inner** function decorated with `csrf_protect`, which performs the CSRF check and processes the upload. The documentation notes the cost: handlers may start receiving the file before the CSRF check has run. For class-based views, the same split is done with `method_decorator` on `dispatch()`. ## Example: stop invoices above 20 MB during parsing For the invoice inbox, a size-limiting handler counts bytes and stops early, marking the request so the view can answer clearly: ```python from django.core.files.uploadhandler import FileUploadHandler, StopUpload class MaxSizeUploadHandler(FileUploadHandler): def __init__(self, request=None, limit=20 * 1024 * 1024): super().__init__(request) self.limit = limit def new_file(self, *args, **kwargs): super().new_file(*args, **kwargs) self.received = 0 def receive_data_chunk(self, raw_data, start): self.received += len(raw_data) if self.received > self.limit: self.request.upload_too_large = True raise StopUpload(connection_reset=True) return raw_data # pass to the default handlers def file_complete(self, file_size): return None # let the next handler build the UploadedFile ``` `StopUpload` does not produce an error response by itself: parsing ends, the oversized file is absent from `request.FILES`, and the view must check the flag or the missing key. ## Testing a handler The test client builds a real multipart body and Django parses it with the same handler list, so a handler installed in the view runs in tests too. Post a `SimpleUploadedFile` one byte over the limit and assert that the view answered as designed and that `request.FILES` did not contain it; post one under the limit and assert the normal path. Use `override_settings(FILE_UPLOAD_HANDLERS=[...])` to test a globally installed handler. ## Other uses - progress reporting keyed by an upload id; - streaming bytes straight to external storage instead of a temp file; - computing a checksum while the data arrives.

  • What happens if a view calls request.upload_handlers.insert(0, h) after request.POST was read?
    Parsing has already replaced the handler list with an `ImmutableList`, so `insert()` raises `AttributeError` with the message that handlers cannot be altered after the upload has been processed. Install handlers as the very first thing in the view, and make sure no middleware or decorator reads `request.POST` earlier.
  • In receive_data_chunk(), what is the difference between returning raw_data and returning None?
    Returning the bytes passes them on to the next handler in the list, so the default handlers still build the `UploadedFile`. Returning `None` means this handler consumed the chunk and later handlers do not receive it; the handler must then produce the file itself in `file_complete()`.

saying these in an interview costs you the question

  • Upload handlers can be changed at any point in the view
  • Returning None from receive_data_chunk passes the data to the next handler
  • csrf_exempt alone is fine; no inner csrf_protect is needed
  • StopUpload makes Django send an error response automatically
  • CsrfViewMiddleware never touches request.POST before the view runs