fetch, CORS и загрузка данных
Что такое CORS?
CORS (Cross-Origin Resource Sharing) — механизм браузера, позволяющий или запрещающий запросы к ресурсам с другого источника (origin = протокол + домен + порт).
Same-Origin Policy (по умолчанию) запрещает скриптам делать запросы к другому origin. CORS расширяет это правило через HTTP-заголовки.
Как работает:
- Браузер добавляет заголовок
Origin: https://mysite.comк запросу - Сервер отвечает:
Access-Control-Allow-Origin: https://mysite.com(или*) - Если заголовка нет — браузер блокирует ответ
Preflight-запрос и “непростые” запросы
Браузер делает preflight (OPTIONS) не для всех кросс-доменных запросов, а только для “непростых”.
Простой запрос — не требует preflight, если одновременно выполнены все условия:
- Метод:
GET,HEADилиPOST Content-Typeтолько:application/x-www-form-urlencoded,multipart/form-dataилиtext/plain- Нет кастомных заголовков (
Authorization,X-Custom-Headerи т.д.)
Непростой запрос — всё остальное:
- Метод
PUT,DELETE,PATCH Content-Type: application/json- Заголовок
Authorization - Любые кастомные заголовки
В таких случаях браузер сначала шлёт OPTIONS-запрос, чтобы спросить у сервера разрешение:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type, AuthorizationКак работает параметр mode в fetch?
mode определяет, как браузер обрабатывает cross-origin запросы.
| Значение | Поведение |
|---|---|
cors | По умолчанию. Разрешает cross-origin, сервер должен вернуть CORS-заголовки |
same-origin | Запрещает любые cross-origin запросы — бросает ошибку |
no-cors | Разрешает отправить запрос, но ответ opaque — тело и заголовки недоступны JS |
navigate | Для навигации браузера, не для кода |
no-cors полезен для аналитики (пиксель трекинга), но не для получения данных.
Картинка не грузится (ошибка 403), но открывается напрямую — как исправить?
Что происходит и почему
Когда вставляешь чужую картинку через <img src="..."> на своей странице, браузер при загрузке картинки автоматически добавляет HTTP-заголовок:
Referer: https://твоя-страница.com/pathМногие серверы используют это для hotlink protection (защита от встраивания картинок с чужих сайтов): они проверяют заголовок Referer и, если он не совпадает с их доменом, возвращают 403 Forbidden.
Когда напрямую открываешь URL картинки в новой вкладке, браузер не добавляет Referer (или добавляет неинформативный) — поэтому картинка открывается.
Решения
-
Скачать картинку и хостить у себя — самый надёжный вариант
-
Убрать
Refererдля картинки:
<img src="..." referrerpolicy="no-referrer">- Убрать
Refererдля всей страницы через мета-тег в<head>:
<meta name="referrer" content="no-referrer">- Проксировать через свой сервер — бэкенд сам запрашивает картинку (без
Refererот твоей страницы) и возвращает её клиенту. Надёжно, но дополнительная нагрузка.
⚠️ Варианты 2 и 3 работают не всегда: некоторые серверы запрещают даже запросы без
Referer.
Скачать большой файл (5 ГБ) с сервера — как?
Вариант 1: Простая ссылка <a download>
<a href="/api/download/bigfile" download="file.zip">Скачать</a>Браузер открывает GET-запрос, сервер отдаёт поток. Браузер пишет прямо на диск, не держа файл в памяти целиком. Плюсы: максимально просто. Минусы: нет контроля из JS (нельзя показать прогресс или обработать ошибку самостоятельно).
Вариант 2: fetch + ReadableStream (с прогресс-баром)
const response = await fetch('/api/bigfile');
const reader = response.body.getReader();
const contentLength = +response.headers.get('Content-Length');
let received = 0;
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
received += value.length;
console.log(`${(received / contentLength * 100).toFixed(1)}%`);
}
const blob = new Blob(chunks);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'file.zip';
a.click();Минусы: все чанки накапливаются в памяти (RAM) до момента сохранения — плохо для 5 ГБ!
Вариант 3: File System Access API (лучший для 5 ГБ)
const fileHandle = await window.showSaveFilePicker({ suggestedName: 'file.zip' });
const writable = await fileHandle.createWritable();
const response = await fetch('/api/bigfile');
await response.body.pipeTo(writable);Данные стримятся прямо на диск, не накапливаясь в памяти. pipeTo соединяет ReadableStream (от fetch) с WritableStream (файл на диске).
Минусы: API работает только в Chrome, Edge; не работает в Firefox/Safari без флагов.
Вариант 4: Range-запросы (возобновляемая загрузка)
// На сервере должен быть заголовок: Accept-Ranges: bytes
const start = 1048576; // продолжить с 1 МБ (уже скачанного)
const response = await fetch(url, {
headers: { 'Range': `bytes=${start}-` }
});Сервер отвечает 206 Partial Content. Можно скачивать параллельно несколькими запросами или возобновить после обрыва.
Требования: сервер должен поддерживать Accept-Ranges: bytes и Content-Range.
См. также
- storage-cookies — origin и same-origin policy
- realtime — SSE и WebSocket
- rest-graphql — REST и GraphQL