Отладка Node через --inspect и DevTools
`node --inspect` поднимает V8 Inspector WebSocket на 127.0.0.1:9229, к которому цепляются DevTools и VS Code; `--inspect-brk` ломает до первой строки. Держи на localhost — этот порт это выполнение кода. Для дешёвых логов бери util.debuglog под NODE_DEBUG.
Сервис падал на старте — значение конфига было undefined ещё до того, как приходил первый запрос. Дежурный инженер добавил node --inspect server.js, открыл DevTools, поставил брейкпоинт на падающей строке и перезапустил. Брейкпоинт не сработал: пока DevTools дотягивался до процесса, тот уже бросил исключение и вышел. Инженер добавил ещё console.log-ов, передеплоил, дождался следующего падения, повторил — час потерян. Фиксом был один флаг, которого он не знал: --inspect-brk, который замораживает процесс до первой строки пользовательского кода и ждёт отладчик, так что баг старта не успевает убежать от подключения.
—inspect поднимает WebSocket, к которому цепляется отладчик
node --inspect app.js не запускает отладчик — он поднимает сервер. Node включает агент V8 Inspector, который открывает WebSocket и печатает строку вроде Debugger listening on ws://127.0.0.1:9229/<uuid>. По умолчанию он биндится на 127.0.0.1:9229 — только localhost, один общеизвестный порт. Всё, что говорит на Chrome DevTools Protocol (CDP — открытый протокол отладки браузера и Node), потом цепляется к этому сокету: Chrome через chrome://inspect, отдельное окно DevTools или JavaScript-отладчик VS Code. Отладчик — это клиент; твой процесс — сервер. Эта рамка объясняет следующие два факта, на которых люди обжигаются.
Первое: подключение асинхронно и занимает реальное стенное время. --inspect даёт твоему коду бежать сразу, так что быстро падающий баг старта успевает закончиться до того, как клиент подключится, — ровно случай из хука. Фикс — --inspect-brk, который включает агент и ломает до первой строки пользовательского кода, паркуя процесс, пока отладчик не подцепится и не скажет продолжить. Бери --inspect для долгоживущего сервера, к которому подцепишься на лету; бери --inspect-brk, когда баг в старте, на верхнем уровне модуля или в чём угодно, что выполняется один раз. (--inspect-wait — золотая середина: ждать подключения, но не ломать.)
Второе: адрес бинда — осознанный выбор. --inspect=0.0.0.0:9229 или --inspect=<публичный-ip> выставляет агент в сеть — читай дальше, потому что это одно изменение и есть разница между отладчиком и удалённым шеллом.
# подцепиться на лету к работающему серверу (localhost:9229)
node --inspect server.js
# заморозить до строки 1 и ждать отладчик — для багов старта
node --inspect-brk server.js
# свой бинд (тут всё ещё localhost; см. раздел безопасности до расширения)
node --inspect=127.0.0.1:9230 server.jsБрейкпоинты, оператор debugger и source maps
После подключения DevTools даёт полный набор: строчные брейкпоинты, условные брейкпоинты (срабатывать только при userId === 42), logpoint-ы (печатают выражение, не останавливаясь — брейкпоинт, который логирует вместо паузы), step over/into/out, живой список watch и Console-REPL, вычисляемый в области видимости остановленного фрейма, так что ты можешь читать и даже менять локальные переменные на брейкпоинте. Голый оператор debugger; в исходнике — это программный брейкпоинт: он ставит паузу, если отладчик подключён, и no-op в противном случае.
Ловушка для TypeScript и любого скомпилированного/собранного кода: отладчик видит сгенерированный JavaScript, так что брейкпоинты и фреймы стека приземляются в dist/app.js, а не в твой src/app.ts. Запускай с --enable-source-maps, чтобы Node читал файлы .js.map и перемапливал трассы стека — и позиции строк отладчика — обратно в исходник. Без этого продовая трасса стека указывает на минифицированную строку 1, колонку 9000, а твой брейкпоинт сидит в коде, который ты никогда не писал.
# сгенерированный JS + source maps → стеки и брейкпоинты мапятся в .ts
node --enable-source-maps --inspect-brk dist/app.js▸Почему это работает
Почему отладчик вообще работает в отдельном клиенте, а не вшит в рантайм? Потому что V8 Inspector выставляет тот же Chrome DevTools Protocol, что использует браузер, — так что те самые DevTools, которые ты уже знаешь (Sources, Console, Memory, Profiler), работают без изменений против серверного процесса, а инструменты вроде VS Code, WebStorm и chrome://inspect все говорят на одном протоколе с одним WebSocket. Цена этой мощи — поверхность атаки из следующего раздела: протокол, который умеет вычислять произвольные выражения в твоём процессе, по определению является каналом выполнения кода.
util.debuglog: логи с нулевой ценой без отладчика
Иногда ты не хочешь останавливаться — ты хочешь трассу, которую можно включить в одном окружении и не платить за неё нигде больше. Это util.debuglog(section). Он возвращает логирующую функцию, которая является no-op, пока переменная окружения NODE_DEBUG не называет эту секцию — в выключенном состоянии она не делает форматирования строк и ничего не пишет, так что её фактически бесплатно оставить в горячих путях. Включай на запуск через NODE_DEBUG=mysection, перечисляй несколько через запятую (NODE_DEBUG=db,http) или используй wildcard-ы (NODE_DEBUG=db*). Вывод идёт в stderr с тегом секции и PID: DB 3245: query took 14ms. Само ядро Node использует ровно этот механизм — NODE_DEBUG=http,net,tls зажигает внутренние трассы рантайма.
import { debuglog } from "node:util";
const log = debuglog("db"); // no-op, пока нет NODE_DEBUG=db
async function query(sql) {
const t = performance.now();
const rows = await pool.query(sql);
log("query took %dms (%d rows)", performance.now() - t, rows.length);
return rows;
}
// запуск: NODE_DEBUG=db node app.js → "DB 3245: query took 14ms (3 rows)"
// запуск: node app.js → тишина, нулевые накладныеНавык — подбирать инструмент под момент. Таблица ниже — сеньорская решётка решений.
| Подход | Что даёт | Цена | Когда |
|---|---|---|---|
—inspect | Подцепиться вживую; брейкпоинты, watch, REPL в области | Ручное подключение; асинхронно — может упустить быстрые падения | Долгоживущий сервер, баг на лету |
—inspect-brk | То же, но пауза до строки 1 | Блокирует, пока не подцепится отладчик | Старт / верх модуля / разовые баги |
NODE_DEBUG + util.debuglog | Трасса в stderr по секциям, без остановки | No-op при выключении; почти нулевая цена | Условная трассировка, безопасно в прод |
console.log | Печать всегда, без настройки | Всегда форматирует + пишет; засоряет логи | Только разовый локальный зонд |
Режим сбоя: порт инспектора — это удалённый шелл
Вот правило, которое превращает инструмент отладки в инцидент: порт инспектора — это поверхность удалённого выполнения кода. Chrome DevTools Protocol включает Runtime.evaluate, который выполняет произвольные выражения внутри твоего процесса. Доки Node прямолинейны: если отладчик забиндён на публичный IP или 0.0.0.0, любой клиент, способный дотянуться до этого адреса, может подключиться без всякой аутентификации и выполнить произвольный код от имени твоего процесса. Нет ни пароля, ни токена — достижимость и есть авторизация.
Отсюда два неоспоримых правила. Никогда не бинди инспектор ни на что, кроме localhost, и никогда не оставляй --inspect включённым в проде. Дефолтный 127.0.0.1 безопасен именно потому, что дотянуться может только локальная машина; --inspect=0.0.0.0 это выбрасывает. Чтобы отладить удалённую машину, не расширяй бинд — туннелируй localhost-в-localhost через SSH (ssh -L 9221:localhost:9229 user@host), чтобы порт никогда не касался публичной сети. Флаг --inspect, случайно вшитый в продовый образ контейнера, или бинд на 0.0.0.0 на хосте с публичным интерфейсом — это полноценный RCE-бэкдор, который ни одно правило фаервола внутри приложения не закроет.
Твой сервис бросает исключение и выходит во время старта, до первого запроса. Почему голый `node --inspect` не ловит это и что ловит?
По умолчанию на что биндится `node --inspect app.js` и почему этот дефолт важен?
Функция в долгоживущем staging-сервере, похожем на прод, время от времени возвращает неверные суммы, но только для определённых пользователей. Ты хочешь посмотреть локальные переменные в момент сбоя, не останавливая каждый запрос. Какой подход подходит лучше?
Расставь шаги, чтобы подцепить DevTools к работающему скрипту и поставить паузу на баге старта, безопасно:
- 1 Запусти процесс с агентом инспектора, ломающим до строки 1: node --inspect-brk app.js
- 2 Node открывает WebSocket V8 Inspector на 127.0.0.1:9229 и печатает ws://-URL, запаркованный до пользовательского кода
- 3 Открой chrome://inspect (или VS Code / DevTools) и подцепись к перечисленной цели 127.0.0.1:9229
- 4 Поставь брейкпоинты (строчный / условный / logpoint) в исходнике; с --enable-source-maps они приземляются в твой .ts
- 5 Возобнови выполнение; процесс добегает до твоего брейкпоинта, где ты смотришь локальные переменные в остановленном фрейме
- 01Почему оставить `--inspect` включённым в проде (или забиндить его на 0.0.0.0) — это риск удалённого выполнения кода, и как вместо этого безопасно отладить удалённую машину?
- 02Ты отлаживаешь TypeScript-сервис, и твои брейкпоинты приземляются в dist/app.js, а не в твой src .ts, а трассы стека указывают на номера строк скомпилированного кода. Что происходит и в чём фикс?
node --inspect не запускает отладчик — он поднимает агент V8 Inspector, WebSocket-сервер, забиндённый по умолчанию на 127.0.0.1:9229, а DevTools, VS Code или chrome://inspect цепляются к нему через Chrome DevTools Protocol. Поскольку подключение занимает стенное время, голый --inspect может упустить быстро падающий баг старта; --inspect-brk ломает до первой строки пользовательского кода и ждёт отладчик, так что баги времени загрузки и разовые не успевают убежать от подключения. После паузы ты получаешь строчные, условные и logpoint-брейкпоинты, шагание, список watch и Console, вычисляемый в области остановленного фрейма; debugger; — тот же брейкпоинт в исходнике. Для скомпилированного или TypeScript-кода запускай с --enable-source-maps, чтобы стеки и брейкпоинты мапились обратно в исходный .ts вместо dist. Когда ты не хочешь останавливаться, util.debuglog(section) даёт трассу с почти нулевой ценой, которая является no-op, пока NODE_DEBUG не называет секцию, — тот же механизм, что ядро использует для NODE_DEBUG=http,net. И одно правило, которое превращает инструмент в инцидент: порт инспектора выполняет Runtime.evaluate, то есть произвольный код, без аутентификации, так что держи его на localhost, туннелируй через SSH для удалённой работы и никогда не бинди на 0.0.0.0 и не выкатывай --inspect в прод. Теперь, когда встретишь краш на старте, который --inspect не поймал, или увидишь 0.0.0.0:9229 в образе контейнера, — ты знаешь, что взять и что убрать.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.