Почему ссылка с download открывает файл вместо скачивания — 3 причины
У тебя кнопка «Скачать отчёт». В коде всё по учебнику:
<a href="https://xyz.supabase.co/storage/v1/object/public/reports/report.pdf" download="Отчёт.pdf">
Скачать
</a>
Жмёшь — и PDF открывается в новой вкладке. Имя «Отчёт.pdf» браузер проигнорировал. С картинкой то же самое: вместо файла в загрузках — картинка на весь экран.
Атрибут download написан правильно. Просто у него есть условия, о которых редко говорят. Вот три причины, начиная с самой частой.
Причина 1. Файл лежит на другом домене
Это почти всегда она. Атрибут download работает только для ссылок с того же домена, что и страница. Ещё для адресов вида blob: и data:. Для всего остального браузер его молча игнорирует. Chrome так делает с 2018 года, остальные — тоже.
Твой сайт на myshop.ru, а файл в Supabase Storage, S3 или на CDN — это другой домен. Браузер рассуждает так: чужой сервер не просил отдавать файл на скачивание, значит, решаю сам. PDF и картинки он умеет показывать — и показывает.
Как проверить. Посмотри на адрес в href. Если домен отличается от адреса в строке браузера хотя бы поддоменом — это оно.
Как починить. Пусть сам сервер скажет «это на скачивание». Для этого есть заголовок ответа:
Content-Disposition: attachment; filename="report.pdf"
Заголовок сильнее атрибута и работает с любого домена. Если в нём есть имя файла, браузер возьмёт его, а не то, что в download.
В Supabase Storage заголовок за тебя добавит опция download:
const { data } = await supabase.storage
.from("reports")
.createSignedUrl("report.pdf", 60, { download: "Отчёт.pdf" });
К адресу допишется параметр download, и сервер отдаст файл как вложение. У getPublicUrl есть такая же опция для публичных бакетов. В S3 то же самое делает параметр ResponseContentDisposition у подписанной ссылки.
Причина 2. Ты качаешь через fetch, но не превращаешь в файл
Иногда файл нужно получить с токеном или сгенерировать на лету. Тогда пишут fetch — и ничего не происходит. Запрос прошёл, данные пришли, а в загрузках пусто. fetch просто приносит байты в память. Файлом на диске они сами не станут.
Как починить. Сделай из ответа временную ссылку blob: — для неё download работает всегда:
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
const blob = await res.blob();
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = "Отчёт.pdf";
link.click();
setTimeout(() => URL.revokeObjectURL(link.href), 1000);
Тут три подвоха.
- Чужой домен должен разрешить запрос. Иначе
fetchупадёт ещё до скачивания. Это та самая ошибка CORS. - Не освобождай ссылку сразу.
revokeObjectURLв той же строке, что иclick(), в некоторых браузерах обрывает скачивание. Небольшая пауза решает. - Не качай так большие файлы.
blobдержит весь файл в памяти вкладки. Отчёт на мегабайт — нормально. Видео на два гигабайта — повесит телефон. Для больших файлов используй способ из первой причины.
Причина 3. Встроенный браузер в приложении
Ссылку открыли из Telegram, Instagram или почтового клиента. Там не настоящий браузер, а встроенное окошко — WebView. Многие из них просто не умеют сохранять файлы: показывают PDF или ничего не делают.
Как проверить. Открой ту же ссылку в обычном Safari или Chrome. Скачалось — дело во встроенном окне.
Как починить. Код тут бессилен. Зато можно подсказать человеку: «Если файл не скачивается, открой страницу в браузере». Встроенные окна обычно прячут такой пункт в меню из трёх точек.
Как попросить ИИ
Нейросеть почти всегда напишет голый <a download> и уверит, что это работает. Дай ей контекст:
сделай кнопку скачать отчётСделай кнопку «Скачать отчёт». Файл лежит в приватном бакете Supabase Storage reports, сайт на другом домене, поэтому атрибут download сам по себе не сработает. Получи ссылку через createSignedUrl на 60 секунд с опцией download и именем Отчёт.pdf, чтобы сервер отдал Content-Disposition attachment. Без fetch и blob: файлы бывают по 200 МБ.Как посмотреть, какой заголовок отдаёт сервер?
Открой DevTools, вкладку Network, нажми на ссылку и выбери запрос к файлу. В Response Headers найди Content-Disposition. Если его нет или там inline — браузер вправе показать файл.
Почему одни файлы скачиваются, а другие открываются?
Браузер открывает то, что умеет показать сам: PDF, картинки, видео, текст. Архив или документ Word он показать не может — и скачивает даже без атрибута. Поэтому баг часто замечают только на PDF.
Короткие уроки-истории, симулятор агента и ежедневная практика — в нашем мобильном приложении. Бесплатно.


