skip to content

In Django, what does FileResponse do for a file download that a plain HttpResponse or StreamingHttpResponse does not?

level: middleimportance: should knowfreq 38%

answer

  1. a StreamingHttpResponse subclass
  2. open in binary, do not close
  3. headers guessed from the file
  4. as_attachment and filename

basics

~20 s

FileResponse streams a binary file-like object in blocks, closes it afterwards, and sets Content-Length, Content-Type and Content-Disposition from the file where it can. as_attachment=True and filename control whether the browser downloads it and under what name.

solid answer

~30 s

`FileResponse` is a `StreamingHttpResponse` subclass specialised for binary files. Given a file opened in `"rb"` mode, it reads it in 4096-byte blocks (or uses the WSGI server's `wsgi.file_wrapper` when available) and closes it when the response finishes, so you must not open it in a `with` block. From the file it derives `Content-Length` when the size is knowable, `Content-Type` from the file name via `mimetypes` (falling back to `application/octet-stream`), and `Content-Disposition`: `attachment` when `as_attachment=True`, otherwise `inline` if a name is known. `filename=` overrides the name. For an `io.BytesIO`, `seek(0)` first. Authorisation is still the view's job.

code

python · 23 lines
python
import io

from django.http import FileResponse, Http404

from .models import StockCount


def stock_count_pdf(request, pk):
    count = StockCount.objects.filter(pk=pk, warehouse__staff=request.user).first()
    if count is None:
        raise Http404
    return FileResponse(
        count.report.open("rb"),
        as_attachment=True,
        filename=f"stock-count-{count.pk}.pdf",
    )


def labels_pdf(request):
    buf = io.BytesIO()
    buf.write(b"%PDF-1.7 ...")  # generated bytes
    buf.seek(0)  # otherwise the response starts at the end
    return FileResponse(buf, filename="labels.pdf")

go deeper

for a junior

Remember to pass a file opened in rb mode, let Django close it, and use as_attachment=True to force a download.

for a middle

Explain which headers set_headers() infers, when Content-Length is missing, and why BytesIO needs seek(0).

for a senior

Keep permission checks in the view, validate paths, and know the limits: no range requests and synchronous reads under ASGI.

for a principal

Decide which downloads Django should serve at all versus handing delivery to a web server or object storage after authorisation.

## Where FileResponse sits `django.http.FileResponse` is declared as `FileResponse(open_file, as_attachment=False, filename="", **kwargs)` and subclasses `StreamingHttpResponse`. That gives it the streaming behaviour (constant memory, no `.content` attribute) plus file-specific work that you would otherwise write by hand: - reading the file in fixed-size blocks (`block_size = 4096`), or letting the WSGI server use `wsgi.file_wrapper` if it provides one; - registering the file's `close()` so it is closed when the response is closed; - computing the three download headers from the file object. With a plain `HttpResponse(f.read())` the whole file is loaded into memory and no headers are inferred. With a bare `StreamingHttpResponse(f)` the body streams, but you would set every header yourself and the iteration would be line-based rather than block-based. ## Header inference, precisely `FileResponse.set_headers()` runs during construction: | Header | How it is derived | |---|---| | `Content-Length` | from seeking to the end of a seekable file, `getbuffer()` on a `BytesIO`, or the size on disk of a named file; omitted if none of these work | | `Content-Type` | guessed from `filename` or the file's `name` with `mimetypes.guess_type()`; `application/octet-stream` if nothing is guessed; untouched if you passed `content_type` | | `Content-Disposition` | `attachment; filename=...` with `as_attachment=True`; `inline; filename=...` otherwise, but only when a file name is available | Two details trip people up: 1. For compressed files the type is set to the archive type (for example `application/gzip`) rather than declaring a `Content-Encoding`, so browsers save the `.gz` instead of silently decompressing it. 2. Non-ASCII file names are encoded with the RFC 6266 `filename*=utf-8''...` form, so a report called `Bestandsliste-Nürnberg.pdf` downloads with the right name. ## Correct usage ```python from django.http import FileResponse return FileResponse(open(path, "rb"), as_attachment=True, filename="stock-count.pdf") ``` Rules of thumb: - open in **binary** mode (`"rb"`); the class is for bytes; - do **not** use `with open(...) as f:` around it, because the block closes the file before the server has read it, while `FileResponse` would close it anyway; - for an in-memory `io.BytesIO`, call `seek(0)` after writing, or the response streams from the end and sends nothing; - pass `filename=` when the file's own name is a temporary path or a UUID. ## Choosing between the response classes | Approach | Memory | Headers | Closes the file | |---|---|---|---| | `HttpResponse(f.read(), content_type=...)` | whole file in RAM | all by hand | only if you close it | | `StreamingHttpResponse(f)` | constant, but iterates by lines | all by hand | yes, through `close()` registration | | `FileResponse(f, as_attachment=True)` | constant, fixed-size blocks | inferred from the file | yes | `HttpResponse` is still reasonable for a few kilobytes of generated content where you already hold the bytes. For anything read from disk or storage, `FileResponse` is the idiomatic choice, and it is also what Django's own development static-file view returns. ## What it does not do - **Authorisation.** It serves whatever file the view opens. The view must check that the user may see the stock report and must never build the path from raw user input without validation. - **Range requests.** Django's `FileResponse` does not implement byte-range responses, so resumable downloads of large media need a web server or storage service in front. - **Async file reads.** Under ASGI the documentation notes that Python's file API is synchronous, so the file must be fully consumed to be served; streaming a large file asynchronously needs a third-party async file library. For large or frequently downloaded files, production setups usually let a web server or object storage deliver the bytes after Django has done the permission check, and keep `FileResponse` for moderate files, generated documents and development.

  • A FileResponse built from an io.BytesIO downloads as an empty file. Why?
    After writing, the buffer position is at the end. `FileResponse` reads from the current position, so there is nothing left to send, and the computed `Content-Length` (`getbuffer().nbytes - tell()`) is zero. Call `buf.seek(0)` before passing it.
  • Why is wrapping FileResponse in a with open(...) block a bug?
    The view returns the response before any bytes are sent; the server iterates it later. Leaving the `with` block closes the file first, so iteration fails on a closed file. `FileResponse` registers the file's `close()` itself and runs it when the response is closed.

saying these in an interview costs you the question

  • FileResponse reads the whole file into memory before sending
  • Open the file in a with-block so FileResponse does not leak it
  • FileResponse checks that the user may access the file
  • Text mode is fine because Django encodes it
  • Content-Disposition is always attachment for FileResponse