NodeJS | setTimeout() | Ошибка — TypeError: msg._implicitHeader is not a function — efim360.ru

NodeJS | setTimeout() | Ошибка - TypeError: msg._implicitHeader is not a function

Работая с таймерами или интервалами в NodeJS можно столкнуться с неожиданной ошибкой падения сервера. В проксирующем веб-сервере nginx на NodeJS-сервер, на клиент может уходить 502 код ошибки HTTP. Причём интересно то, что сам setTimeout() отрабатывает выверенное ему время задержки. На клиент ошибка падает спустя определённую задержку таймера.

Эта ошибка формулируется средой выполнения кода как:

TypeError: msg._implicitHeader is not a function

Нижнее подчёркивание говорит нам о том, что "_implicitHeader" является внутренним свойством объекта "msg" и вы никак не можете его изменить. В общем, это встроенное свойство в NodeJS, а не ваш плагин, модуль или что-то ещё. То есть ошибка идёт изнутри.

 

По какой причине может возникнуть данная ошибка?

Наиболее вероятный вариант - это потеря ОБЪЕКТА КОНТЕКСТА ВЫЗОВА ФУНКЦИИ.

Представим, что у нас есть простой NodeJS-сервер, который на все запросы хочет отвечать с задержкой в 5 секунд. Фрагмент кода из нашего NodeJS-сервера:

const http = require('http');

const hostname = '192.168.0.1';

const port = 9999;

const server = http.createServer((req, res) => {

  res.statusCode = 200;

  res.setHeader('Content-Type', 'text/html; charset=utf-8');

  setTimeout(res.end, 5000, 'Hello after 5 seconds');

});

server.listen(port, hostname, () => {

  console.log(`Сервер стартовал http://${hostname}:${port}/ - ${new Date().toLocaleString()}`);

});

 

Нас интересует запись "setTimeout(res.end, 5000, 'Hello after 5 seconds');". Именно она вызывает данную ошибку.

Сама функция setTimeout() вызывается напрямую в коде без объявления. Мы только подключили модуль "http". Это говорит о том, что setTimeout() является свойством ГЛОБАЛЬНОГО ОБЪЕКТА JavaScript, который в NodeJS называется global.

Принадлежность к глобальному объекту даёт максимальную видимость этому свойству. Это значит, что setTimeout() можно вызывать откуда угодно в коде программы. НО!!! Всегда нужно помнить, что вызов метода setTimeout() будет осуществлён в контексте ГЛОБАЛЬНОГО ОБЪЕКТА.

Так вот, вся проблема в том, что при передачи первым параметром в setTimeout() ССЫЛКИ на функцию end() мы имеем объект контекста "global", а не "res". Мы не вызываем end(), а просто указываем путь до end()!

Когда внутри вызова setTimeout() среда выполнения кода начнёт вычислять ОБЪЕКТ КОНТЕКСТА ВЫЗОВА для end(), то он привяжется к ОБЪЕКТУ КОНТЕКСТА ВЫЗОВА самой setTimeout(). То есть end() будет пытаться вызваться от имени "global", а не "res" когда пройдёт нужная задержка по времени. Это именно потому, что мы НЕ ВЫЗЫВАЕМ, а ПЕРЕДАЁМ функцию в качестве параметра. Так как end() - это функциональное свойство (метод), то внутри него работает конструкция this, указывающая на ТЕКУЩИЙ ОБЪЕКТ КОНТЕКСТА.

Ну а когда дело доходит до вызова end(), то внутри ключевое слово this принимает значение global. Тогда глобальный объект не понимает, что с этим нужно делать и просто валится в ошибку. В нашем случае падает весь сервер. Если вы задержки прописываете для определённых маршрутов, то отвалится какой-то адрес на клиенте спустя задержку.

 

Как устранить ошибку?

Чтобы избавиться от ошибки потери ОБЪЕКТА КОНТЕКСТА ВЫЗОВА ФУНКЦИИ, нужно к передаваемой функции end() принудительно привязать ПРАВИЛЬНЫЙ ОБЪЕКТА КОНТЕКСТА ВЫЗОВА.

Все экземпляры Объектов Функций (Function Objects) в JavaScript наследуют 6 свойств от своего прототипа:

  1. apply ( thisArg, argArray )
  2. bind ( thisArg, ...args )
  3. call ( thisArg, ...args )
  4. constructor
  5. toString ( )
  6. [Symbol.hasInstance]
Свойства экземпляров класса Function в JavaScript
Свойства экземпляров класса Function в JavaScript

Нас будет интересовать одно свойство, которое отвечает ТОЛЬКО ЗА ПРИВЯЗКУ НУЖНОГО ОБЪЕКТА КОНТЕКСТА к функции end().

Это свойство называется bind(). Оно ВОЗВРАЩАЕТ новую функцию с уже определённым ОБЪЕКТОМ КОНТЕКСТА.

Теперь мы можем переписать наше выражение так:

 

    setTimeout(res.end.bind(res), 5000, 'Hello after 5 seconds');

   

 

Выражение ".bind(res)" вызовется сразу (так как круглые скобки) ещё внутри передачи параметров в setTimeout(). В качестве привязки будет выступать ОБЪЕКТ ОТВЕТА NodeJS-сервера, который у нас живёт под именем (идентификатором) "res". То есть, мы получим новую функцию с уже определённым ОБЪЕКТОМ КОНТЕКСТА её ВЫЗОВА.

Выражение "res.end" - это просто ссылка на функцию end() внутри которой есть this. Слово "res" тут выступает просто в качестве отправной точки маршрута для поиска местонахождения кода функции end() и ничего кроме. В передаче параметров, функция end() НЕ ВЫЗЫВАЕТСЯ.

Когда таймер отсчитает 5 секунд, тогда переданная функция (она уже вычислена с новым контекстом) будет вызвана и подхватит строку 'Hello after 5 seconds' и вернёт её на клиент в качестве ОТВЕТА на ЗАПРОС.

 

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

Стандарт ECMAScript - https://tc39.es/ecma262/multipage/

Стандарт NodeJS - https://nodejs.org/en/