5.9.1. File Downloads
Table of Contents
Understanding File Downloads
Downloading files from a backend is the mirror operation of uploads. Instead of the client sending file data, the client asks for a file and the server sends back its contents in a safe and controlled way.
In this chapter you will see how file downloads work conceptually, how responses should be built, and what to watch out for regarding security and performance.
Basic Idea of a File Download
When a client wants to download a file, it still sends a normal HTTP request, usually a GET request for a specific URL, for example:
GET /files/report.pdf HTTP/1.1
Host: example.comYour backend then:
- Locates the requested file, for example on disk or in object storage.
- Checks if the user is allowed to download it.
- Reads the file contents.
- Sends back an HTTP response with:
- The file bytes in the body.
- Appropriate headers that tell the browser what the file is, its size, and how to handle it.
The browser then decides whether to display or save the file, based on the headers and file type.
Key HTTP Headers for Downloads
Two headers are especially important for file downloads: Content-Type and Content-Disposition. Others like Content-Length and caching headers are also useful.
Content-Type
Content-Type describes what kind of file is being sent. Some common values:
| File type | Content-Type |
|---|---|
| HTML | text/html |
| Plain text | text/plain |
| JSON | application/json |
application/pdf | |
| JPEG image | image/jpeg |
| PNG image | image/png |
| ZIP archive | application/zip |
| Generic binary | application/octet-stream |
If you are not sure which type to use, you can use application/octet-stream as a safe generic binary type.
Content-Disposition
Content-Disposition tells the browser how to present the file. It can suggest either:
inlinethe browser may try to display it, orattachmentthe browser should offer it as a download.
Example header:
Content-Disposition: attachment; filename="report.pdf"
This usually triggers a "Save As" dialog with the file name report.pdf.
If you use inline:
Content-Disposition: inline; filename="report.pdf"The browser may open the PDF directly in a tab, while still using the file name for saving.
Important rule
Always set a safe and explicit Content-Disposition when sending user-controlled file names to avoid header injection and confusing behavior. Sanitize the file name before including it in the header.
Content-Length
Content-Length is the size of the file in bytes. It allows the client to know how much data to expect.
Example:
Content-Length: 1048576For streaming or chunked responses you might not set this header explicitly, the web framework or server can handle that.
Simple Download Example
At a high level, a simple download handler in pseudocode looks like this:
def download_file(request, filename):
# 1. Build a safe path
path = build_safe_path(base_dir, filename)
# 2. Check permissions
if not user_can_access(request.user, path):
return Response(status=403)
# 3. Read file bytes
content = read_bytes(path)
# 4. Detect content type
content_type = guess_content_type(filename)
# 5. Return response
return Response(
body=content,
headers={
"Content-Type": content_type,
"Content-Disposition": f'attachment; filename="{filename}"',
},
status=200,
)The details depend on your framework, but the same core steps apply.
Inline Display vs Forced Download
Sometimes you want the browser to display the file, sometimes you want it to be downloaded.
When to Use `inline`
Use inline when:
- You want images to show in the browser.
- You want PDFs to open in the browser viewer.
- You serve HTML pages directly.
Example:
Content-Disposition: inline; filename="photo.jpg"
Content-Type: image/jpegWhen to Use `attachment`
Use attachment when:
- You deliver generated reports (PDF, CSV, Excel).
- You provide backups or export archives.
- You send files that are not easily displayed in the browser.
Example:
Content-Disposition: attachment; filename="orders-2024-01.csv"
Content-Type: text/csv
If you are not sure what the user wants, attachment is usually safer, because it avoids accidentally executing or embedding content.
Dynamic vs Static Downloads
Your backend can send files that:
- Already exist on disk (static files), or
- Are generated on the fly (dynamic files).
Static File Download
For static files, you usually:
- Store them in a directory on disk or in object storage.
- Look them up by some identifier or file name.
- Stream the bytes to the client.
Example scenarios:
- Download profile pictures.
- Download previously uploaded documents.
- Download static assets like manuals or templates.
Dynamic File Download
For dynamic files, the process is:
- Generate the content in memory or in a temporary file.
- Optionally compress it (for example ZIP).
- Send it directly to the client without permanently storing it.
Examples:
- Generate a PDF invoice based on database data.
- Export a CSV of all orders for a user.
- Create a zip archive of several files for a one-time download.
Dynamic downloads often have URLs that include query parameters to specify what to generate, for example:
GET /exports/orders.csv?from=2024-01-01&to=2024-01-31Streaming Large Files
If the file is small, you can read it fully into memory and then send it. For large files this is not efficient. Instead you use streaming.
With streaming, the backend:
- Opens the file.
- Reads a small chunk of bytes at a time, for example 64 KB.
- Writes each chunk to the HTTP response as it is read.
- Closes the file when done.
This has two advantages:
- Memory use stays low, independent of file size.
- The client can start receiving data immediately.
Pseudocode for streaming:
def file_iterator(path, chunk_size=65536):
with open(path, "rb") as f:
while True:
chunk = f.read(chunk_size)
if not chunk:
break
yield chunk
def download_large_file(request, filename):
path = build_safe_path(base_dir, filename)
iterator = file_iterator(path)
return StreamingResponse(
iterator,
media_type="application/octet-stream",
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)The actual implementation depends on your web framework, but the idea is the same: you return an iterator or generator of bytes instead of a single in-memory blob.
Important rule
Use streaming for large files. Reading huge files fully into memory can:
- Increase memory usage per request.
- Reduce server capacity.
- Cause crashes or out-of-memory conditions.
Security Considerations for Downloads
File downloads can easily introduce security problems if you are not careful. Always validate what is being requested and who is requesting it.
Prevent Path Traversal
A common attack is to ask for a file outside the intended directory using .. segments, for example:
GET /files/../../etc/passwdIf your code simply concatenates paths like this:
path = base_dir + "/" + requested_filenamean attacker might access sensitive system files.
You need to normalize the path and check that it still points inside the allowed directory, or better, never trust raw paths from users at all.
Safer approaches:
- Store only internal IDs in URLs, for example
/files/12345, and map the ID to a path in your database. - If you must accept a path or name, use library functions to resolve the final path and then check that it is a descendant of your base directory.
Enforce Authorization
You must check that the user is allowed to download a given file. Examples:
- Only the owner can download their private documents.
- Only admins can download certain reports.
- Only users in a specific group can access shared files.
You can implement this by storing metadata for each file in your database, such as owner ID and access rules, and checking against the current authenticated user.
Important rule
Never trust only the file path or name as a security boundary. Always check authorization based on your own application rules before sending a file.
Avoid Leaking File System Structure
Do not expose full physical paths or detailed error messages, such as:
File not found: /var/app/data/users/12/private/secret.pdfInstead, send a generic message:
File not foundand log the detailed information server side.
Sanitize File Names in Headers
The filename parameter in the Content-Disposition header must be carefully handled. If you include user-provided file names directly, they might inject special characters.
Examples of unsafe names:
my"report.pdfreport\nX-Injected-Header: value
Use a whitelist of characters, replace unsafe characters, or use a library that builds a safe header value. A common simple rule is to allow only letters, numbers, periods, dashes, and underscores in the output file name.
Range Requests and Partial Downloads
HTTP supports range requests, which allow clients to resume downloads or fetch part of a file. The client sends a header like:
Range: bytes=1000-1999to request a specific byte range of the file. The server responds with:
HTTP/1.1 206 Partial Content
Content-Range: bytes 1000-1999/123456Implementing range requests is more advanced, but useful for:
- Large media files.
- Resume support in download managers.
- Video streaming.
Some web servers handle range requests for static files automatically, but if you generate or stream files manually you may need to implement range logic yourself.
Download Links and Authentication
Download URLs can be:
- Public, accessible to everyone.
- Protected, requiring authentication and authorization.
- Temporary, using signed URLs that work only for a limited time.
Authenticated Downloads
For protected downloads, the browser usually includes a session cookie or token when requesting the file, so your backend can detect the user.
For example:
GET /user-files/12345 HTTP/1.1
Cookie: session=abc123Your handler then:
- Authenticates the user from the cookie or token.
- Verifies that they are allowed to access file 12345.
- Returns the file response.
Temporary Signed URLs
Sometimes you want to give a user a link that:
- Does not require a session cookie.
- Works only for a short time.
- Cannot be easily guessed.
A common pattern is a signed URL, for example:
https://example.com/files/download?id=12345&expires=1700000000&signature=abcxyzThe signature is computed on the server side with a secret key based on the parameters. When the request comes in, the server re-computes and checks the signature and the expiration time. If they match and the time is valid, the file is sent.
This is especially common when you use external object storage like S3, where you may generate presigned URLs that point directly to the storage service.
Handling Different File Types
Files can behave differently in browsers depending on their Content-Type and the Content-Disposition header. Here are some common patterns.
Downloading a CSV Report
For a CSV export you usually want a file download:
Content-Type: text/csv; charset=utf-8
Content-Disposition: attachment; filename="orders.csv"
The browser will offer to download orders.csv. Many spreadsheet programs will open it easily.
Serving Images
For images you might prefer inline display:
Content-Type: image/jpeg
Content-Disposition: inline; filename="photo.jpg"The browser will show the image directly when the URL is opened.
If instead you want the user to download the image, you can set:
Content-Disposition: attachment; filename="photo.jpg"Downloading Binary Files
If you do not know the exact content type, or you have custom binary formats, you can use:
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="backup.bin"This tells the browser it is generic binary data and should be downloaded.
Error Handling for Downloads
When a download fails, you should return appropriate HTTP status codes.
Common cases:
| Situation | Status code | Notes |
|---|---|---|
| File not found | 404 | Generic message to the client |
| User not authenticated | 401 | Often with WWW-Authenticate header |
| User not authorized | 403 | The file exists but is not accessible |
| Invalid request parameters | 400 | For example invalid file ID or range |
| Internal error reading file | 500 | Log details, send generic error to client |
Make sure to not send partial or corrupted file data silently. If reading fails partway through a stream, log the error and let the connection fail, or design retry behavior for clients when appropriate.
Progress and Large Downloads on the Client Side
From the backend perspective you mostly stream bytes, but it is useful to understand that clients may:
- Show progress bars based on
Content-Length. - Retry or resume using range requests.
- Abort downloads mid-way, which your server should tolerate.
Your backend should:
- Set
Content-Lengthwhen the size is known. - Support cancellation or simply close the file handle when the connection drops.
- Log partial downloads when relevant, for example for auditing.
Putting It All Together
A robust file download feature in a backend typically includes:
- A clean URL design, for example
/files/{file_id}or/users/{id}/exports. - Authentication and authorization checks for private files.
- Safe mapping from identifiers to file locations.
- Proper headers:
Content-TypeContent-DispositionContent-Lengthwhen known- Streaming for large files to save memory.
- Careful error handling and logging.
- Protection against path traversal and header injection.
Once you understand these building blocks, implementing file downloads in any backend framework becomes straightforward.
Views: 7
KAHIBARO