Суть Чтение реальных сниппетов Express и NestJS — баг порядка middleware, singleton с состоянием запроса, издержки bubbling request-scope и цикл зависимостей — и выбор фикса с наибольшим рычагом.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min
Баги middleware и DI диагностируются в коде связывания и порядке регистрации, а не в бизнес-логике. Прочитайте каждый сниппет, предскажите, что сломается под нагрузкой, и выберите фикс, к которому senior-инженер тянется первым.
Цель
Отработайте цикл, который запускает каждый инцидент middleware/DI: прочитать порядок pipeline или связывание, предсказать сбой (утёкший ответ, межпользовательская гонка данных, пересборка графа на каждый запрос, неразрешимый цикл) и выбрать структурный фикс вместо заплатки.
Поток неаутентифицированных запросов с большими телами разгоняет CPU до 100%. Что не так с этим порядком и каков фикс?
Heads-up Express выполняет middleware последовательно в порядке регистрации, каждый вызывает next(). Порядок — именно тот рычаг: работа перед точкой отказа оплачивается запросами, которые всё равно отклоняются.
Heads-up Регистрация последним пропустила бы каждый запрос к обработчику до ограничения. Дешёвые отказы должны быть рано — издержки этого порядка в парсинге тел для запросов, которые auth/limit всё равно отбросят.
Heads-up Ничто в сниппете не показывает, что authenticate ведёт себя неправильно. Видимый дефект — порядок pipeline: самый тяжёлый шаг идёт перед двумя дешёвыми точками отказа.
Сниппет 2 — поле в singleton
@Injectable() // scope по умолчанию: singletonexport class AuditService { private currentUser?: User; // ставится на запрос, читается позже setUser(u: User) { this.currentUser = u; } record(action: string) { log(`${this.currentUser?.id} did ${action}`); }}
Викторина
Completed
Под конкурентной нагрузкой журнал аудита приписывает действия не тому пользователю. Почему и каков самый чистый фикс?
Heads-up Порча происходит до логирования: общее поле currentUser перезаписывается конкурентным запросом. Фикс — перестать хранить состояние запроса на singleton, а не менять логгер.
Heads-up readonly вообще запретит setUser и не решит совместного доступа — экземпляр всё ещё один на процесс. Проблема — само поле; передавайте пользователя аргументом.
Heads-up У методов нет scope; они есть только у провайдеров. Гонка — общее поле singleton. Либо передавайте значение аргументом, либо делайте провайдер request-scoped.
Сниппет 3 — request-scoped логгер
@Injectable({ scope: Scope.REQUEST }) // один экземпляр на запросexport class ContextLogger { /* хранит request id */ }@Injectable() // задуман как singletonexport class PaymentService { constructor(private log: ContextLogger) {} // внедряет request-scoped зависимость}// AppController внедряет PaymentService, OrderService внедряет PaymentService...
Викторина
Completed
После этого изменения нагрузочный тест показывает повышенные аллокации и латентность во многих несвязанных сервисах. Почему?
Heads-up Request scope не трогает кэширование запросов. Издержки структурные: он распространяется вверх по цепочке внедрения и заставляет пересоздавать каждого потребителя на запрос.
Heads-up Request-scoped экземпляры отбрасываются после запроса. Издержки здесь — пересоздание зависимого подграфа на каждый запрос, а не утечка.
Heads-up Он не может остаться изолированным: любой singleton, внедряющий request-scoped провайдер, сам должен стать request-scoped, как и его потребители. Lifetime всплывает вверх по графу.
Heads-up forwardRef откладывает резолвинг одной стороны, чтобы разорвать дедлок в рантайме, но сохраняет циклический дизайн и может разрешаться недетерминированно в разных окружениях. Это запах, а не структурный фикс.
Heads-up Scope про lifetime, а не про порядок. У цикла нет валидного порядка конструирования при любом scope; смена lifetime не даёт графу топологического порядка.
Heads-up Иногда два зацикленных класса должны быть одним, но дисциплинированный фикс — обычно вынести общую зависимость в третий класс, оставив ответственности раздельными и сделав граф ациклическим.
Итог
Каждый инцидент middleware/DI читается в связывании, а не в логике. Порядок middleware — рычаг стоимости и безопасности: ставьте дешёвые отказы (rate limit, auth) перед дорогой работой вроде парсинга тела. Singleton должен быть без состояния относительно запроса; хранение полей на запрос гонит данные между конкурентными пользователями, поэтому передавайте значение аргументом. Request scope всплывает вверх по цепочке внедрения и пересобирает подграф на запрос, так что тянитесь к нему только при реальной необходимости. А у цикла зависимостей нет топологического порядка — структурный фикс в выносе общего третьего класса, а не в подавлении резолвера через forwardRef.