KAHIBARO
Discord Login Register

5.9.1. File Downloads

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:

text
GET /files/report.pdf HTTP/1.1
Host: example.com

Your backend then:

  1. Locates the requested file, for example on disk or in object storage.
  2. Checks if the user is allowed to download it.
  3. Reads the file contents.
  4. 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 typeContent-Type
HTMLtext/html
Plain texttext/plain
JSONapplication/json
PDFapplication/pdf
JPEG imageimage/jpeg
PNG imageimage/png
ZIP archiveapplication/zip
Generic binaryapplication/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:

Example header:

text
Content-Disposition: attachment; filename="report.pdf"

This usually triggers a "Save As" dialog with the file name report.pdf.

If you use inline:

text
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:

text
Content-Length: 1048576

For 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:

python
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:

Example:

text
Content-Disposition: inline; filename="photo.jpg"
Content-Type: image/jpeg

When to Use `attachment`

Use attachment when:

Example:

text
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:

Static File Download

For static files, you usually:

  1. Store them in a directory on disk or in object storage.
  2. Look them up by some identifier or file name.
  3. Stream the bytes to the client.

Example scenarios:

Dynamic File Download

For dynamic files, the process is:

  1. Generate the content in memory or in a temporary file.
  2. Optionally compress it (for example ZIP).
  3. Send it directly to the client without permanently storing it.

Examples:

Dynamic downloads often have URLs that include query parameters to specify what to generate, for example:

text
GET /exports/orders.csv?from=2024-01-01&to=2024-01-31

Streaming 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:

  1. Opens the file.
  2. Reads a small chunk of bytes at a time, for example 64 KB.
  3. Writes each chunk to the HTTP response as it is read.
  4. Closes the file when done.

This has two advantages:

Pseudocode for streaming:

python
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:

text
GET /files/../../etc/passwd

If your code simply concatenates paths like this:

python
path = base_dir + "/" + requested_filename

an 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:

Enforce Authorization

You must check that the user is allowed to download a given file. Examples:

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:

text
File not found: /var/app/data/users/12/private/secret.pdf

Instead, send a generic message:

text
File not found

and 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:

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:

text
Range: bytes=1000-1999

to request a specific byte range of the file. The server responds with:

text
HTTP/1.1 206 Partial Content
Content-Range: bytes 1000-1999/123456

Implementing range requests is more advanced, but useful for:

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:

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:

text
GET /user-files/12345 HTTP/1.1
Cookie: session=abc123

Your handler then:

  1. Authenticates the user from the cookie or token.
  2. Verifies that they are allowed to access file 12345.
  3. Returns the file response.

Temporary Signed URLs

Sometimes you want to give a user a link that:

A common pattern is a signed URL, for example:

text
https://example.com/files/download?id=12345&expires=1700000000&signature=abcxyz

The 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:

text
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:

text
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:

text
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:

text
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:

SituationStatus codeNotes
File not found404Generic message to the client
User not authenticated401Often with WWW-Authenticate header
User not authorized403The file exists but is not accessible
Invalid request parameters400For example invalid file ID or range
Internal error reading file500Log 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:

Your backend should:

Putting It All Together

A robust file download feature in a backend typically includes:

  1. A clean URL design, for example /files/{file_id} or /users/{id}/exports.
  2. Authentication and authorization checks for private files.
  3. Safe mapping from identifiers to file locations.
  4. Proper headers:
    • Content-Type
    • Content-Disposition
    • Content-Length when known
  5. Streaming for large files to save memory.
  6. Careful error handling and logging.
  7. Protection against path traversal and header injection.

Once you understand these building blocks, implementing file downloads in any backend framework becomes straightforward.

Views: 7

Comments

Please login to add a comment.

Don't have an account? Register now!