HTTP клиент на JavaScript: делаем запросы правильно — продвинутый уровень
Базовые HTTP-запросы с помощью fetch или axios — это лишь вершина айсберга. В реальных продакшн-приложениях на Node.js сталкиваешься с десятками нюансов: пулы соединений, повторные попытки при сбоях, перехватчики для логирования, стриминг больших данных и тонкая настройка таймаутов. В этой статье мы разберём продвинутые техники работы с HTTP-клиентами в JavaScript. Вы узнаете, как управлять keep-alive, реализовать надёжный retry-механизм, использовать перехватчики и создавать кастомные агенты. Материал ориентирован на разработчиков, которые уже знакомы с основами HTTP и хотят писать более эффективный и отказоустойчивый код.
1. Управление соединениями: Keep-Alive и кастомные агенты
По умолчанию Node.js использует глобальный пул HTTP-агентов. Однако для высоконагруженных систем важно контролировать количество одновременных соединений и их переиспользование. Рассмотрим настройку keep-alive с помощью модуля http и библиотеки undici.
Пример с кастомным агентом (Node.js built-in):
const http = require('http');
const https = require('https');
// Создаём кастомный агент с пулом соединений
const agent = new http.Agent({
keepAlive: true,
maxSockets: 50, // Максимум 50 одновременных сокетов
maxFreeSockets: 10, // Не более 10 свободных в пуле
timeout: 60000, // Таймаут бездействия сокета (мс)
});
const options = {
hostname: 'api.example.com',
port: 80,
path: '/data',
agent: agent,
};
const req = http.get(options, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => console.log('Ответ получен:', data));
});
req.on('error', (e) => console.error('Ошибка:', e.message));
Современный подход с undici (быстрее и легче):
const { Client, Pool } = require('undici');
// Создаём пул с keep-alive
const client = new Pool('https://api.example.com', {
connections: 100,
pipelining: 10,
keepAliveTimeout: 30000,
});
// Выполняем запрос
client.request({
method: 'GET',
path: '/data',
}).then(({ statusCode, body }) => {
console.log('Статус:', statusCode);
body.setEncoding('utf8');
body.on('data', console.log);
}).catch(err => console.error(err));
Использование кастомных агентов позволяет избежать проблем с утечками сокетов и улучшает производительность за счёт переиспользования соединений.
2. Надёжный механизм повторных попыток (Retry)
Сети нестабильны. Даже временные сбои могут привести к потере данных. Правильная стратегия retry должна учитывать тип ошибки, количество попыток и экспоненциальную задержку. Реализуем универсальный retry-декоратор для любого HTTP-клиента.
const axios = require('axios');
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
async function withRetry(requestFn, retries = 3, baseDelay = 1000) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await requestFn();
} catch (error) {
// Не повторяем на клиентские ошибки (4xx)
if (error.response && error.response.status >= 400 && error.response.status < 500) {
throw error;
}
if (attempt === retries) {
throw error; // Исчерпаны все попытки
}
const delay = baseDelay * Math.pow(2, attempt - 1) + Math.random() * 1000;
console.log(`Попытка ${attempt} не удалась. Ждем ${delay}ms...`);
await sleep(delay);
}
}
}
// Использование
async function fetchData() {
const response = await withRetry(() => axios.get('https://api.example.com/data'), 5, 500);
return response.data;
}
fetchData().then(console.log).catch(console.error);
Ключевые моменты: исключение 4xx-ошибок (их повторять бессмысленно), экспоненциальная задержка с джиттером (для предотвращения "грозы запросов") и логирование каждой попытки.
3. Перехватчики (Interceptors) для логирования, аутентификации и кеширования
Перехватчики позволяют внедрить сквозную логику без изменения основного кода запросов. В axios это встроенная возможность, а для fetch можно написать свою обёртку.
Пример с axios (запрос + ответ):
axios.interceptors.request.use(config => {
console.log(`[${new Date().toISOString()}] ${config.method.toUpperCase()} ${config.url}`);
// Добавляем токен авторизации
config.headers.Authorization = `Bearer ${process.env.API_TOKEN}`;
return config;
}, error => Promise.reject(error));
axios.interceptors.response.use(response => {
console.log(`Статус: ${response.status} для ${response.config.url}`);
// Простое кеширование GET-запросов
if (response.config.method === 'get' && response.status === 200) {
cache.set(response.config.url, response.data);
}
return response;
}, error => {
console.error('Ошибка ответа:', error.message);
return Promise.reject(error);
});
Реализация для fetch (обёртка):
const fetchWithInterceptors = (input, init = {}) => {
// Interceptor на запрос
const modifiedInit = {
...init,
headers: {
...init.headers,
'X-Request-ID': crypto.randomUUID(),
},
};
console.log(`Запрос: ${input}`);
return fetch(input, modifiedInit).then(response => {
// Interceptor на ответ
console.log(`Ответ: ${response.status} для ${input}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
});
};
Перехватчики незаменимы для централизованной обработки ошибок, добавления метрик и автоматического обновления токенов.
4. Стриминг больших данных и эффективная обработка
При работе с большими ответами (например, JSON-потоки или файлы) держать весь ответ в памяти — плохая практика. Используйте стримы для обработки по частям.
Стри