Node.js 24.20.0 QUIC: get_reader не теряет buffered data после FIN. Изолированный test/control, измеримая матрица, дерево решения, stop-line и обезличенный пакет поддержки.
Что именно изменилось
Документированная граница для этой страницы: get_reader возвращает buffered data, даже когда FIN уже получен и stream больше не is_readable в прежнем смысле. Она отвечает на запрос «как проверить чтение QUIC stream body после FIN в Node.js 24.20.0» и не означает, что любой похожий сбой имеет ту же причину. Наблюдаемая боль сформулирована узко: приложение начинает читать после FIN и получает пустой body, хотя bytes полностью приняты. Сначала запишите версию runtime, ожидаемые состояния и конечный timeout. Только затем запускайте локальный fixture. Успешный результат подтверждает одну ветку Node.js 24.20.0; он не доказывает массовость проблемы, совместимость всего приложения или необходимость срочного production-обновления.
Паспорт воспроизведения
Test-паспорт T18-24: loopback peer отправляет marker body, оставляет writable side открытой, reader запрашивается только после FIN marker. Он использует только синтетические markers и временные объекты. Измеритель: «read start phase × FIN seen × buffered bytes × delivered bytes × writable open». Каждое поле записывается до интерпретации, чтобы ожидаемый вывод не менял наблюдение. Не добавляйте реальный трафик, базу, ключи, cookies, account identifiers или пользовательские файлы. Если требуемое поле нельзя получить безопасно, отмечайте его неизвестным и блокируйте вывод, а не расширяйте доступ. Peer отправляет известный marker body, завершает sending side, а приложение намеренно откладывает получение reader до фиксации FIN. Сравните hash и length позднего чтения с ранним control. Пустой late result нельзя автоматически объяснять FIN: сначала подтвердите, что bytes были приняты в loopback fixture.
Контрольная ветка
Control не является повтором test: reader начинается до первого byte на новой session. Для обеих веток закрепите один Node binary, архитектуру, locale, clock source и порядок операций. Снимите baseline A, выполните B один раз, закройте ресурсы и повторите A2. Если A2 расходится с A, опыт оставил состояние — дальнейшие повторы только запутают диагностику. Нельзя одновременно менять input shape, codec, policy, transport option и timing: в таком опыте причинность не восстанавливается.
Как заполнить матрицу исходов
Рабочая таблица этой темы: «read start phase × FIN seen × buffered bytes × delivered bytes × writable open». Не заменяйте её словами «быстрее», «сломалось» или «вроде прошло». Для promise фиксируйте settled state и reason, для stream — точный event order и counters, для buffer/codec — constructors, lengths и equality, для QUIC — только обезличенные codes и transitions. Повторите test с теми же значениями ровно один раз. Расхождение повторов означает неопределённость, а не редкий подтверждённый дефект.
Решение по результату
Основное правило: ранний и поздний reader получают один marker — loss boundary закрыта; late пустой — сохраняем case. Есть ещё три обязательные ветки. Если test и control падают одинаково, исправьте fixture. Если control чист, но test не воспроизводится, вывод остаётся неопределённым. Если после cleanup A2 отличается от A, остановитесь и найдите оставшийся handle, cache entry или session. Нельзя переносить узкий результат на другую версию Node.js, OpenSSL backend, ОС, network path или пользовательский workload без нового сравнимого опыта.
Когда немедленно остановиться
Предметная stop-line: не принимать FIN за отсутствие уже принятых данных и не использовать пользовательский payload. Также останавливайтесь при crash вне child process, зависании без deadline, неожиданном внешнем соединении, изменении файла за temp root, запросе повышенных прав или появлении приватных данных в error/log. Нельзя добиваться зелёного результата отключением TLS validation, безразмерным buffer, глобальным exception handler, бесконечным retry или уничтожением рабочего состояния. Нулевой безопасный вывод лучше удобной выдуманной причины.
Что передать в поддержку
Минимизированный пакет: event order, marker hash/length, delivered bytes, side states и Node.js version. Добавьте Moscow timestamp, архитектуру, точный `node --version`, один command line без секретных flags и строки матрицы A/B/A2. Уберите абсолютные домашние пути, hostnames, содержимое key material, payload, IP, authorization headers и full dumps. Получатель должен повторить один transition без доступа к вашей инфраструктуре. Если пакет требует рабочую базу или внешний сервис, он ещё не минимизирован.
Почему T18-24 — отдельная статья
Предметная трасса T18-24 начинается с состояния «приложение начинает читать после FIN и получает пустой body, хотя bytes полностью приняты». Зафиксируйте его без объяснения причины, затем соберите ровно такой стенд: loopback peer отправляет marker body, оставляет writable side открытой, reader запрашивается только после FIN marker. Наблюдения раскладываются по колонкам «read start phase × FIN seen × buffered bytes × delivered bytes × writable open»; пустая колонка делает опыт незавершённым. После полного cleanup запустите независимую ветку «reader начинается до первого byte на новой session». Сопоставлять разрешено только строки с одинаковыми version, architecture и input markers. Предметный критерий разбора: ранний и поздний reader получают один marker — loss boundary закрыта; late пустой — сохраняем case. Если он не выполнен буквально, сохраните статус unknown и не переносите гипотезу в production. Особая граница безопасности этого case: не принимать FIN за отсутствие уже принятых данных и не использовать пользовательский payload. Для эскалации оставьте только «event order, marker hash/length, delivered bytes, side states и Node.js version»; остальные runtime details удалите. Такой маршрут отделяет изменение «get_reader возвращает buffered data, даже когда FIN уже получен и stream больше не is_readable в прежнем смысле» от соседних симптомов с иным event order, buffer ownership, cancellation path или error class. Самостоятельность T18-24 задаёт не слово Node.js, а неделимая комбинация: намерение «как проверить чтение QUIC stream body после FIN в Node.js 24.20.0»; боль «приложение начинает читать после FIN и получает пустой body, хотя bytes полностью приняты»; boundary «get_reader возвращает buffered data, даже когда FIN уже получен и stream больше не is_readable в прежнем смысле»; test «loopback peer отправляет marker body, оставляет writable side открытой, reader запрашивается только после FIN marker»; control «reader начинается до первого byte на новой session»; таблица «read start phase × FIN seen × buffered bytes × delivered bytes × writable open». Решение применяется только как «ранний и поздний reader получают один marker — loss boundary закрыта; late пустой — сохраняем case», а право остановиться — «не принимать FIN за отсутствие уже принятых данных и не использовать пользовательский payload». Пакет поддержки ограничен полями «event order, marker hash/length, delivered bytes, side states и Node.js version». Соседняя статья с другим lifecycle, buffer ownership, error class или transport event не отвечает на этот вопрос. После опыта удалите fixture и подтвердите, что process, listener, timer, stream или session не остались активными. Идентификатор наблюдения: node-24200-quic-reader-fin-buffered-data.
Материал подготовлен редакцией VOne с помощью ИИ по открытым официальным и первичным источникам; факты, даты и ссылки перепроверены. Реальные пользовательские данные не использовались.
Источники и проверка
- Node.js 24.20.0 release notes проверено 2026-08-29
- Node.js pull request #63946 проверено 2026-08-29
Информация актуальна на дату публикации. Правила сервисов, приложений и сетей могут меняться.