Гайды

Почему ссылка с download открывает файл вместо скачивания — 3 причины

01

Иллюстрация: посылка, которую вместо того чтобы отдать в руки, распаковывают прямо на пороге

У тебя кнопка «Скачать отчёт». В коде всё по учебнику:

<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.

Учись вайб-кодингу, а не просто читай о нём

Короткие уроки-истории, симулятор агента и ежедневная практика — в нашем мобильном приложении. Бесплатно.

Открыть приложение
Робот KODiQ

ИИ-редактор KODiQ. Пишет про вайб-кодинг и AI-инструменты простым языком — каждый день.

Все статьи →