Скачивание файлов в Node.js — методы и обработка ошибок

Почему стоит скачивать файлы локально
Скачивание файлов в локальное хранилище даёт несколько практических преимуществ: быстрый доступ без интернета, полный контроль и владение данными, возможность дальнейшей обработки (индексация, бэкап, трансформация). Этот подход полезен для офлайн-работ, бэкап-утилит, миграций и ETL-процессов.
Ключевые термины
- Stream: поток данных, удобный для записи/чтения больших файлов без загрузки всего в память.
- Callback/Promise: способы обработки асинхронных операций в Node.js.
Скачивание без сторонних библиотек
Для простого и контролируемого скачивания можно использовать три встроенных модуля: fs, https, path. Модуль fs отвечает за запись файлов, https — за выполнение HTTPS-запросов, path — за работу с путями и извлечение имени файла.
Импорт необходимых модулей (пример):
const https = require('https');
const fs = require('fs');
const path = require('path');Простой пример из исходного описания (переведён):
const filename = path.basename(url);
https.get(url, (res) => {
})
const fileStream = fs.createWriteStream(filename);
res.pipe(fileStream);
fileStream.on('finish', () => {
fileStream.close();
console.log('Download finished');
});Этот код рабочий для базовых случаев, но в реальных приложениях важно учитывать ошибки потоков, проверку HTTP-кода ответа, редиректы и ограничения по таймаутам.
Надёжная функция скачивания (советированная версия)
Ниже — улучшенный, промис-ориентированный вариант функции скачивания, учитывающий проверку статуса ответа и обработку ошибок потоков. Он работает для HTTPS и HTTP (понадобится модуль http для plain-HTTP):
const http = require('http');
const https = require('https');
const fs = require('fs');
const path = require('path');
const { URL } = require('url');
function downloadFile(urlString, destFolder = '.') {
return new Promise((resolve, reject) => {
try {
const url = new URL(urlString);
const protocol = url.protocol === 'https:' ? https : http;
const filename = path.basename(url.pathname) || 'download';
const destination = path.join(destFolder, filename);
const request = protocol.get(url, (res) => {
// Обработка HTTP-редиректа (302/301)
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
// рекурсивно следуем редиректу
resolve(downloadFile(res.headers.location, destFolder));
return;
}
if (res.statusCode < 200 || res.statusCode >= 300) {
reject(new Error(`HTTP error: ${res.statusCode} ${res.statusMessage}`));
res.resume(); // очистить поток
return;
}
const fileStream = fs.createWriteStream(destination);
res.pipe(fileStream);
fileStream.on('finish', () => {
fileStream.close(() => resolve(destination));
});
fileStream.on('error', (err) => {
// удалить частично записанный файл
fileStream.close(() => {
fs.unlink(destination, () => reject(err));
});
});
});
request.on('error', (err) => reject(err));
request.setTimeout(30000, () => {
request.abort();
reject(new Error('Request timed out'));
});
} catch (err) {
reject(err);
}
});
}Пояснения к реализации:
- Использование URL позволяет корректно извлечь pathname и имя файла. path.basename предотвращает запись в произвольные поддиректории, но всё равно нужно валидировать результат.
- Проверяем статусы 300–399 для редиректов, 200–299 как признак успеха.
- Обработка ошибок потоков и удаление частично записанного файла повышают надёжность.
- Таймаут защищает от зависания запроса.
Запуск функции и скачивание нескольких файлов
Если вы передаете URL-ы через командную строку, используйте process.argv:
const args = process.argv;
const urls = args.slice(2);
Promise.all(urls.map(u => downloadFile(u, './files')))
.then(results => console.log('Downloaded:', results))
.catch(err => console.error('Error downloading files:', err));Запуск из терминала:
node script.js https://example.com/a.jpg https://example.com/b.pdfОбработка ошибок при скачивании
Общие ошибки при скачивании: сетевые сбои, неверный URL, ошибка записи на диск, недоступный ресурс, редиректы, отсутствие прав на запись, таймауты.
Советы по обработке ошибок:
- Не полагайтесь только на try/catch для асинхронного кода; используйте промисы и обработчики событий.
- Всегда слушайте ‘error’ у потоков (stream.on(‘error’, …)).
- Проверяйте res.statusCode и реагируйте на коды 4xx/5xx.
- Обрабатывайте редиректы (3xx) и потенциально ограничивайте глубину редиректов.
- Очищайте частично записанные файлы при ошибках.
- Добавьте таймауты и ограничьте число попыток при повторных ошибках.
Пример простого механизма повторных попыток (экспоненциальная задержка)
async function downloadWithRetries(url, dest, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await downloadFile(url, dest);
} catch (err) {
if (attempt === maxRetries) throw err;
const delay = Math.pow(2, attempt) * 500; // 500ms, 1000ms, 2000ms...
await new Promise(r => setTimeout(r, delay));
}
}
}Try/catch и асинхронность
Важно: try/catch ловит синхронные ошибки и ошибки await-операторов. Если функция возвращает промис и вы не используете await, то try/catch не поймает ошибку — используйте .catch или await внутри async-функций.
Пример неправильного использования (не уловит асинхронную ошибку):
try {
downloadFile(url); // возвращает промис
} catch (error) {
console.log(error); // не сработает для ошибки внутри промиса
}Правильный вариант:
(async () => {
try {
await downloadFile(url);
} catch (err) {
console.error('Download failed', err);
}
})();Использование сторонних библиотек
Иногда проще и безопаснее использовать готовые пакеты, которые уже решают вопросы редиректов, таймаутов и параллелизации.
Пример с пакетом download (npm):
npm install download
const download = require('download');
const args = process.argv;
const urls = args.slice(2);
async function downloadFile(urls){
await Promise.all(urls.map((url) => download(url, "files")));
}
downloadFile(urls);Альтернативы: axios, got, node-fetch. Многие из них поддерживают промежуточную обработку потоков и дополнительные опции (таймаут, заголовки, авторизация). Выбор зависит от требований: нужна ли поддержка HTTP/2, прокси, retry-логика, контроль над потоками.
Когда встроенный подход не подходит
- Если нужен сложный контроль параллелизма с очередями и приоритетами — лучше взять библиотеку задач/очередей.
- Если требуется авторизация (OAuth, токены) и сложная логика повторных попыток — часто удобнее использовать axios/got.
- Для загрузки очень больших файлов (десятки ГБ) следует учитывать ограничения файловой системы и обеспечить контроль использования диска и памяти.
Безопасность и конфиденциальность
- Проверяйте URL-ы от внешних источников: не позволяйте записывать файлы с опасными именами или в системные папки.
- Обрабатывайте и нормализуйте имена файлов (например, удалять ../ и запрещённые символы).
- Не выполняйте загруженные файлы без дополнительной проверки и валидации.
- Если загружаете конфиденциальные данные, храните их шифрованными и следите за правами доступа.
- При работе с пользовательскими URL-ами учитывайте риск SSRF и ограничивайте набор разрешённых доменов.
Рольовые чек-листы перед релизом
Разработчик:
- Написал обработку ошибок для потоков.
- Добавил проверку HTTP-кодов.
- Реализовал таймаут и retry стратегию.
DevOps:
- Убедился, что у сервиса есть место на диске и соответствующие права.
- Настроил мониторинг дискового пространства и ошибок записи.
QA:
- Протестировал скачивание больших и маленьких файлов.
- Смоделировал сетевые сбои и проверил поведение повторных попыток.
Критерии приёмки
- Файлы корректно скачиваются и сохраняются в указанную папку.
- При ошибке записываются понятные сообщения и частично записанные файлы удаляются.
- Таймауты работают, и поток не висит бесконечно.
- При передаче нескольких URL-ов все файлы корректно скачиваются параллельно или последовательно в зависимости от настроек.
Факты и подсказки
- HTTP-успех: коды 200–299.
- Редиректы: коды 300–399 — нужно следовать или отбросить в зависимости от политики.
- Всегда слушайте события ‘error’ у потоков.
Совместимость и миграция
Код с использованием потоков fs и модулей http/https работает в современных версиях Node.js. При миграции убедитесь, что версия Node.js поддерживает синтаксис async/await и URL API (Node.js 8+), и при необходимости обновите зависимые пакеты.
Примеры типичных ошибок и их решения
- Файл не скачивается, но нет ошибки: проверьте res.statusCode и обработчик res.resume() при ошибках.
- Частично скачанный файл остаётся на диске: добавьте слушатель fileStream.on(‘error’) и fs.unlink для удаления.
- Неправильное имя файла с query-параметрами: используйте path.basename(new URL(url).pathname).
Краткое резюме
Скачивание файлов в Node.js легко организовать с помощью встроенных модулей, но для продакшн-решений важно добавить обработку ошибок, таймауты, поддержку редиректов и валидацию имён файлов. При сложной логике загрузок задумайтесь о готовых библиотеках, которые упрощают retry-политику и параллелизм.
Важно
Перед тем как скачивать файлы от незнакомых источников, всегда проверяйте URL, фильтруйте домены и не выполняйте загруженное содержимое автоматически.
Конец статьи