NodeJS | Как прервать долгий HTTP-запрос на сайт, спустя определённое время без основного ответа?

NodeJS | Как прервать долгий HTTP-запрос на сайт, спустя определённое время без основного ответа?

У нас есть NodeJS код, который посылает запрос на сайт. То есть наше NodeJS приложение по сути является клиентом, а не сервером.

 

Проблемы для NodeJS в роли клиента, связанные с долгими ответами сайтов

Некоторые сайты могут долго отвечать на HTTP-запросы, которые рассылает NodeJS в роли клиента. Если мы посылаем большое количество запросов, то что происходит?

То при долгих ответах сайтов, наша оперативная память физического сервера может переполняться от хранения временных переменных, которые участвуют в формировании запросов. Сами объекты HTTP-запросов занимают место в памяти. Пока запрос не завершён, память будет хранить его данные. Как вы понимаете, память на любом сервере имеет предел.

По умолчанию, объект HTTP-запроса до самого конца ожидает получить ответ (в случае успешного соединения). Это нормально, потому что задача любого приложения — это 100% получение данных, где бы они не были. Но в некоторых задачах это правило можно не соблюдать. К такой задаче я отношу сканирование сайтов в интернете. Чем дольше сканер «залипает» на каком-то адресе, тем больше потенциальных «быстрых адресов» он пропускает. Пускай я лучше пропущу 5% «долгих адресов«, чем потеряю 40% времени для  сканирования «быстрых адресов«. Для меня выгоднее быстрее получить информацию о новых адресах в сети интернет, которых нет в моей базе. Ценность новой информации в этой задаче для меня важнее.

Чем больше запросов без ответов, тем сильнее заполняется оперативная память. Это может негативно отразиться на остальных процессах физического сервера, ввиду отсутствия свободной памяти. В конечном итоге менеджер процессов PM2 может просто перезагрузить NodeJS приложение, что тоже не хорошо.

У нас есть определённые алгоритмы, которые получают строки HTTP-ответов. Эту задачу нужно выполнять быстрее. Стояние на месте не подходит. Каждый день появляется новая информация, поэтому нужно успевать.

 

Как останавливать выполнение затянутого HTTP-запроса NodeJS при долгом отсутствии HTTP-ответа от сервера сайта?

Нам нужен метод destroy() для объекта HTTP-запроса, который будет вызываться при помощи setTimeout(). Для нашей задачи мы будем прерывать выполнение запроса при ожидании ответа в более чем 10 секунд. То есть если мы не дождёмся основного ответа в течении 10 секунд, то мы принудительно завершим выполнение запроса.

Это значит, что событие ‘close‘ осуществится гораздо раньше, чем могло бы быть в случае долгого ответа. Это значит, что событие ‘response‘ не осуществится для данного HTTP-запроса.

Осталось понять в каком месте кода вызывать этот таймаут. Самым подходящий местом можно считать «следующую» команду после отправки HTTP-запроса методом end(). То есть сразу после end(). и после всех слушателей событий для данного HTTP-запроса.

Также важно передать в setTimeout() объект контекста вызова для метода destroy(). В противном случае получим ошибку, потому что контекст привяжется к globalthis).

 

    setTimeout(q.destroy.bind(q), 10000);

   

 

Для привязки «объекта контекста вызова» мы используем метод bind(). Под «q» у нас живёт сам HTTP-запрос.

При таком подходе у нас принудительно сработает событие ‘error‘ для HTTP-запроса.

Если метод destroy() вызывается до назначения сокета, следующие события будут сгенерированы в следующем порядке:

  • (req.destroy() вызвали здесь)
  • error‘ с ошибкой с сообщением ‘Error: socket hang up‘ и свойством code равным ‘ECONNRESET
  • close

Если метод destroy() вызывается до успешного подключения, следующие события будут сгенерированы в следующем порядке:

  • socket
  • (req.destroy() вызвали здесь)
  • error‘ с ошибкой с сообщением ‘Error: socket hang up‘ и свойством code равным ‘ECONNRESET
  • close

С консольными логами и дополнительной визуализацией это выглядит примерно так:

Принудительно завершили долгий HTTP-запрос на сайт из NodeJS через 10 секунд после отправки
Принудительно завершили долгий HTTP-запрос на сайт из NodeJS через 10 секунд после отправки

 

В обработчике события ‘error‘ можно написать логику для дальнейших действий. Но важно здесь то, что мы точно знаем, что запрос завершился принудительно через установленное таймаутом время, без полученного ответа. У объекта ошибки можно получить значение свойства code.

Если бы мы не прерывали запрос, а адреса небыло бы в интернете, то код ошибки назывался бы ‘ENOTFOUND‘.

 

Информационные ссылки

Стандарт NodeJShttps://nodejs.org/api/

Стандарт NodeJS — Модуль HTTPS — Метод request() — Синтаксис 1 — https://nodejs.org/api/https.html#httpsrequestoptions-callback

Стандарт NodeJS — Модуль HTTPS — Метод request() — Синтаксис 2 — https://nodejs.org/api/https.html#httpsrequesturl-options-callback

Стандарт NodeJS — Модуль Events (События) — https://nodejs.org/api/events.html

Стандарт NodeJS — Слушатель событий ON — https://nodejs.org/api/events.html#emitteroneventname-listener