HTTP клиент на JavaScript: продвинутые техники запросов в Node.js

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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-потоки или файлы) держать весь ответ в памяти — плохая практика. Используйте стримы для обработки по частям.

Стри

Блоги

Рекомендаций по книгам