# How to Use Flask send_file to Serve User-Uploaded PDFs: Avoiding Common Pitfalls

> Learn how to use Flask send_file to serve user-uploaded PDFs securely. Avoid common pitfalls like path traversal and empty responses by using send_from_directory and setting the correct mimetype.

- Repository: [Pallets/flask](https://github.com/pallets/flask)
- Tags: how-to-guide
- Published: 2026-02-19

---

**Always use `send_from_directory` instead of raw user paths, explicitly set `mimetype="application/pdf"`, and ensure file pointers are seeked to the start to prevent empty responses or path traversal vulnerabilities.**

Serving user-uploaded PDFs with Flask's `send_file` utility requires careful attention to security headers and file handling. While the `send_file` function in [`src/flask/helpers.py`](https://github.com/pallets/flask/blob/main/src/flask/helpers.py) provides a convenient wrapper around Werkzeug's file-sending capabilities, improper use of path arguments or MIME types can result in 404 errors, empty responses, or path traversal vulnerabilities. This guide examines the most common pitfalls when using Flask send file to deliver PDFs and provides code-backed solutions straight from the Flask source.

## 1. Path Traversal Vulnerabilities and Unsafe User Input

### The Risk of Raw User-Supplied Paths

`send_file` assumes the supplied path is trusted. Supplying a path that came from a request parameter can expose the server to path-traversal attacks (e.g., `../../../etc/passwd`) and will often result in a "file not found" error because the file lives outside the expected directory. The Flask source explicitly warns about this behavior:

- **Source:** *Never pass file paths provided by a user… Use `send_from_directory`* – [`src/flask/helpers.py`](https://github.com/pallets/flask/blob/main/src/flask/helpers.py#L402-L405)

### Solution: Use send_from_directory with Safe Join

Store uploads in a fixed upload folder (e.g., `app.config["UPLOAD_FOLDER"]`) and serve them with `send_from_directory`, which sanitizes the path using `werkzeug.security.safe_join` before resolving the file.

```python
from flask import Flask, send_from_directory, abort
import os

app = Flask(__name__)
app.config["UPLOAD_FOLDER"] = os.path.join(app.instance_path, "uploads")
app.config["USE_X_SENDFILE"] = True          # optional, for large files

@app.route("/download/<path:filename>")
def download_pdf(filename):
    # `send_from_directory` sanitises the path and prevents traversal.

    try:
        return send_from_directory(
            app.config["UPLOAD_FOLDER"],
            filename,
            mimetype="application/pdf",
            as_attachment=True,
            download_name=filename,          # ensure a safe name

        )
    except Exception as e:
        app.logger.error(f"Failed to serve {filename}: {e}")
        abort(404)

```

## 2. MIME Type Detection Failures

### Explicitly Setting application/pdf

When the `mimetype` argument is omitted, Flask attempts to guess the type from the filename. If the filename lacks a `.pdf` extension or the system guess fails, the response defaults to `application/octet-stream`, causing browsers to mishandle the PDF or prompt for download incorrectly.

- **Source:** *If not provided, it will try to detect it from the file name* – [`src/flask/helpers.py`](https://github.com/pallets/flask/blob/main/src/flask/helpers.py#L37-L38)

**Avoidance:** Always explicitly set `mimetype="application/pdf"` when serving PDFs, or ensure uploaded files retain their original extensions.

### Handling Content-Disposition for Downloads vs Inline

By default, `as_attachment=False` sends `Content-Disposition: inline`, which instructs browsers to render the PDF within the page. Some browsers may block this due to Content Security Policy (CSP) restrictions or missing plugins, leading users to believe the file was not delivered.

- **Source:** *`as_attachment` indicates to a browser that it should offer to save the file* – [`src/flask/helpers.py`](https://github.com/pallets/flask/blob/main/src/flask/helpers.py#L39-L42)

**Avoidance:** For downloadable PDFs, pass `as_attachment=True` and optionally provide a safe `download_name`.

```python
return send_file(
    path,
    mimetype="application/pdf",
    as_attachment=True,
    download_name="report.pdf",
)

```

## 3. File Object State and Binary Mode

### Seeking to the Start of File-Like Objects

When passing an opened file object (e.g., a `BytesIO` buffer built on the fly), Flask expects the pointer to be at the start of the data. If the pointer remains at the end after writing, the response body will be empty.

- **Source:** *Make sure the file pointer is seeked to the start of the data* – [`src/flask/helpers.py`](https://github.com/pallets/flask/blob/main/src/flask/helpers.py#L33-L36)

**Avoidance:** Always call `seek(0)` on file-like objects before passing them to `send_file`.

```python
from flask import send_file
from io import BytesIO

def generate_report():
    # Build PDF in memory...

    pdf_bytes = b"%PDF-1.4 ..."

    pdf_stream = BytesIO(pdf_bytes)
    pdf_stream.seek(0)                       # <-- important!

    return send_file(
        pdf_stream,
        mimetype="application/pdf",
        as_attachment=True,
        download_name="report.pdf",
    )

```

### Ensuring Binary Read Mode

When opening physical files for `send_file`, always use binary mode (`"rb"`). Text mode will corrupt binary PDF data and may raise encoding errors.

## 4. Performance and Server Configuration

### Enabling X-Sendfile for Large PDFs

If the WSGI server supports `X-Sendfile` (Apache) or `X-Accel-Redirect` (Nginx) and `app.config["USE_X_SENDFILE"] = True`, Flask instructs the server to transfer the file directly from disk without loading it into Python memory. Forgetting to enable this flag on a compatible server results in Flask reading large PDFs into memory, causing high CPU and memory usage.

- **Source:** *If the HTTP server supports “X‑Sendfile”, configuring Flask with `USE_X_SENDFILE = True` will tell the server to send the given path* – [`src/flask/helpers.py`](https://github.com/pallets/flask/blob/main/src/flask/helpers.py#L29-L31)

**Avoidance:** Set `app.config["USE_X_SENDFILE"] = True` **and** configure the web server accordingly.

### Cache Control and Conditional Responses

`send_file` enables conditional responses (ETag / `If-Modified-Since`) when a real file path is given. Disabling `conditional` or misconfiguring cache headers can cause browsers to repeatedly download large PDFs or receive unexpected 304 responses.

- **Source:** *`conditional` enables conditional and range responses* – [`src/flask/helpers.py`](https://github.com/pallets/flask/blob/main/src/flask/helpers.py#L43-L45)  
- **Source:** *`max_age` controls `Cache‑Control`* – [`src/flask/helpers.py`](https://github.com/pallets/flask/blob/main/src/flask/helpers.py#L50-L53)

**Avoidance:** Keep `conditional=True` (default) and set `max_age` appropriately, or rely on the app-wide default via `app.get_send_file_max_age`.

```python
@app.route("/cached/<path:name>")
def cached_pdf(name):
    return send_from_directory(
        app.config["UPLOAD_FOLDER"],
        name,
        mimetype="application/pdf",
        as_attachment=False,
        max_age=86400,        # 1 day client-side caching

    )

```

## 5. Relative Paths and Working Directory Issues

If you pass a relative path to `send_file`, Flask resolves it against the process’s current working directory (CWD), which may differ between development, testing, and production environments (e.g., when using a WSGI server like Gunicorn). This frequently causes "file not found" errors for uploaded PDFs.

- **Source:** *The path is relative to the current working directory if a relative path is given* – [`src/flask/helpers.py`](https://github.com/pallets/flask/blob/main/src/flask/helpers.py#L33-L35)

**Avoidance:** Use absolute paths constructed with `os.path.join(app.root_path, ...)` or, preferably, use `send_from_directory` which prefixes the safe base directory automatically.

## 6. Filename Encoding and Special Characters

Older Flask versions encoded filenames in Latin-1, which broke Unicode PDF names. Since Flask 1.0.3, filenames are normalized to ASCII for broader compatibility, but non-ASCII characters in `download_name` can still cause encoding issues in certain browsers.

- **Source:** *Filenames are encoded with ASCII instead of Latin-1 for broader compatibility* – [`src/flask/helpers.py`](https://github.com/pallets/flask/blob/main/src/flask/helpers.py#L84-L86)

**Avoidance:** Clean or slugify filenames before passing them to `download_name`, or ensure your application handles UTF-8 filename encoding explicitly.

## Summary

- **Never trust user paths:** Always use `send_from_directory` with a fixed upload folder to prevent path traversal attacks, as `send_file` assumes trusted input ([`src/flask/helpers.py`](https://github.com/pallets/flask/blob/main/src/flask/helpers.py#L402-L405).
- **Set explicit MIME types:** Pass `mimetype="application/pdf"` to avoid `application/octet-stream` defaults that confuse browsers ([`src/flask/helpers.py`](https://github.com/pallets/flask/blob/main/src/flask/helpers.py#L37-L38).
- **Control disposition:** Use `as_attachment=True` and a safe `download_name` to force downloads rather than inline rendering that may be blocked by CSP ([`src/flask/helpers.py`](https://github.com/pallets/flask/blob/main/src/flask/helpers.py#L39-L42).
- **Manage file pointers:** When using file-like objects (e.g., `BytesIO`), always `seek(0)` before calling `send_file` to avoid empty responses ([`src/flask/helpers.py`](https://github.com/pallets/flask/blob/main/src/flask/helpers.py#L33-L36).
- **Optimize delivery:** Enable `USE_X_SENDFILE` for large PDFs on compatible servers (Nginx/Apache) to offload file transfer from Python memory ([`src/flask/helpers.py`](https://github.com/pallets/flask/blob/main/src/flask/helpers.py#L29-L31).
- **Handle caching:** Leverage conditional responses (`conditional=True`) and set appropriate `max_age` to prevent redundant downloads ([`src/flask/helpers.py`](https://github.com/pallets/flask/blob/main/src/flask/helpers.py#L43-L45), L50-L53).

## Frequently Asked Questions

### Why is my PDF showing as blank or empty when using Flask send_file?

If you are passing a file-like object (such as `BytesIO`) to `send_file`, the file pointer is likely positioned at the end of the buffer after writing. Flask reads from the current position, resulting in an empty response body. According to the source in [`src/flask/helpers.py`](https://github.com/pallets/flask/blob/main/src/flask/helpers.py#L33-L36), you must call `seek(0)` on the object before passing it to `send_file`.

### How do I prevent path traversal attacks when serving user-uploaded files?

Never pass raw user-supplied filenames directly to `send_file`, as it assumes the path is trusted and resolves relative paths against the current working directory. Instead, use `send_from_directory`, which sanitizes the filename using `werkzeug.security.safe_join` against a fixed base directory. The Flask source explicitly warns against user paths in [`src/flask/helpers.py`](https://github.com/pallets/flask/blob/main/src/flask/helpers.py#L402-L405).

### Should I use send_file or send_from_directory for user uploads?

For user-uploaded content, always prefer `send_from_directory`. While `send_file` is suitable for static assets or dynamically generated file objects, `send_from_directory` adds essential security checks that prevent directory traversal. It also handles the conversion of relative paths to absolute paths relative to the specified directory, avoiding issues where the current working directory differs between development and production environments.

### Why does my PDF download with the wrong filename or garbled characters?

Flask encodes filenames as ASCII for broader compatibility (since version 1.0.3), which can cause issues with non-ASCII characters in `download_name`. If the filename contains Unicode characters that cannot be encoded to ASCII, the browser may display garbled text or a generic name. Always sanitize or slugify user-provided filenames before passing them to `download_name`, or explicitly handle RFC 5987 encoding for UTF-8 filenames.