Чтобы браузер скачал файл вместо его отображения, необходимо установить заголовок ответа Content-Disposition со значением attachment. Если этого не сделать или указать inline, браузер попытается открыть файл в новой вкладке или на странице.

Корректная настройка заголовка ответа (Response Header)

Заголовок Content-Disposition должен быть включен в HTTP-ответ сервера перед отправкой тела запроса. Формат строки: Content-Disposition: attachment; filename="имя_файла".

Имя файла должно быть закодировано, если содержит пробелы или специальные символы. Используйте функцию encodeURIComponent для JavaScript или экранирование в коде на сервере (например, PHP header() или Python response.headers).

Пример корректного заголовка: Content-Disposition: attachment; filename="отчет_2024.pdf".

Если имя файла содержит кириллицу, браузеры могут отображать его некорректно. Рекомендуется использовать ASCII-совместимые имена или передавать оригинальное имя в дополнительном поле disposition-filename* (RFC 5987), хотя поддержка этого поля варьируется.

Влияние параметра Content-Disposition на поведение клиента

Значение attachment: браузер показывает диалоговое окно «Сохранить как» или автоматически сохраняет файл в папку по умолчанию, если пользователь не отключил этот механизм.

Значение inline: браузер пытается открыть файл с помощью встроенного просмотрщика (например, PDF-вьюера) или предлагает скачать, если встроенный просмотрщик отсутствует. Для изображений это стандартное поведение без заголовка.

Отсутствие заголовка Content-Disposition: браузер решает самостоятельно, как поступить с файлом. Обычно он проверяет расширение файла и тип контента. Если тип указывает на текст (text/plain) или изображение (image/png), файл откроется в новой вкладке. Для бинарных данных (application/octet-stream) часто происходит скачивание.

Важно: даже при наличии заголовка attachment, браузер может игнорировать его, если тип контента явно указывает на отображаемый формат.

Типы значений для параметра filename и их совместимость

ASCII-имена: работают во всех браузерах. Пример: file.txt, data.csv.

UTF-8 с экранированием: современные браузеры (Chrome, Firefox, Safari) поддерживают имена в UTF-8, но для максимальной совместимости используйте ASCII или RFC 5987.

Проблемные символы: пробелы, скобки, двоеточия требуют кодирования. Например, «Мой файл.txt» лучше передать как «My%20file.txt» или использовать экранирование в зависимости от языка программирования.

Расширения файлов: браузер часто игнорирует расширение в заголовке и использует своё собственное на основе типа контента. Если вы хотите принудительно задать расширение, используйте тип контента application/octet-stream.

Обработка случаев, когда браузер игнорирует заголовок

Сценарий 1: Тип контента image/png. Браузер всегда попытается отобразить изображение, игнорируя attachment. Решение: использовать тип application/octet-stream или application/pdf для принудительного скачивания.

Сценарий 2: Пользователь ранее выбрал «Всегда открывать файлы этого типа». В этом случае браузер не показывает диалог сохранения даже при наличии заголовка attachment.

Сценарий 3: Кэширование. Если файл уже кэширован, браузер может использовать сохраненную версию без проверки заголовков. Очистка кэша или использование уникальных имен файлов решает проблему.

Сценарий 4: Мобильные устройства. На iOS и Android поведение может отличаться. Safari на iOS часто игнорирует attachment для некоторых типов контента.

Сценарии использования: прямая ссылка vs форма загрузки

Прямая ссылка (GET): при запросе к URL с файлом браузер проверяет заголовок. Если он отсутствует или указан inline, файл откроется. Для скачивания обязательно нужен attachment.

Форма загрузки (POST): если форма отправляет файл на сервер, сервер должен вернуть заголовок Content-Disposition: attachment в ответе 200 OK или 201 Created.

Множественные файлы: при загрузке нескольких файлов через multipart/form-data каждый файл обрабатывается отдельно. Заголовок устанавливается для каждого chunk ответа.

Динамическая генерация: если файл создается на лету (например, отчет по запросу), установите заголовок перед отправкой тела ответа.

Проверка корректности работы в разных браузерах

Тестирование: используйте инструменты разработчика (F12) для проверки заголовков ответа. Отправьте запрос и посмотрите, какой статус и заголовки возвращает сервер.

Браузеры Chrome и Firefox: корректно обрабатывают attachment с ASCII-именами. UTF-8 работает в последних версиях.

Safari на macOS/iOS: может игнорировать attachment для изображений и PDF. Используйте тип application/octet-stream для принудительного скачивания.

Edge и Opera: поведение аналогично Chrome, но с некоторыми нюансами при работе с кириллицей.

Ограничения безопасности при передаче имен файлов

Не передавайте имена файлов напрямую из запроса пользователя без проверки. Это может привести к атакам типа path traversal или выполнению кода.

Валидация: проверяйте расширение файла на сервере, а не только по имени. Используйте whitelist разрешенных расширений.

Экранирование: используйте функции кодирования для имен файлов перед установкой заголовка. В PHP — header('Content-Disposition: attachment; filename="' . escapeshellarg($filename) . '"');

Хранение: не сохраняйте файлы с именами, которые могут содержать вредоносные символы. Генерируйте уникальные имена на сервере.

Вопросы и ответы

Можно ли использовать Content-Disposition для скачивания видео?

Да, но только если тип контента не указывает на воспроизведение. Используйте application/octet-stream и заголовок attachment. Браузеры могут игнорировать это для популярных форматов (mp4, webm), поэтому лучше проверять поведение в каждом браузере.

Как заставить браузер скачать файл без диалога «Сохранить как»?

Это невозможно гарантировать во всех браузерах. Пользователь может настроить автоматическое сохранение, но это не является функцией сервера. Единственный способ — использовать attachment и надеяться на настройки пользователя.

Что делать, если браузер открывает файл вместо скачивания?

Проверьте тип контента. Если он image/png или application/pdf, браузер попытается отобразить файл. Измените тип на application/octet-stream или добавьте заголовок Content-Disposition: attachment.

<?php
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="report.pdf"');
echo file_get_contents('report.pdf');
?>
from flask import Response

@app.route('/download')
def download_file():
    return Response(
        open('file.pdf', 'rb').read(),
        mimetype='application/pdf',
        headers={'Content-Disposition': 'attachment; filename=file.pdf'}
    )