0.167.47
Fixed
- MCP-коннект без таймаута блокировал запуск приложения ( — P1; инцидент продакшена chat.a2v.space → 502). После обновления инстанса до v0.167.46 сервис не открыл порт 14888: goroutine-дамп показал
main → mcp.Manager.Start → connectAll → Connect → Initialize → sendHTTP → Transport.getConn, заблокированный навсегда. Причина сети: round-robin DNS api.z.ai с двумя A-рекордами, один из которых — чёрная дыра (TCP принимается middlbox’ом, данные никогда не ACKаются); SSRF-диалер брал только первый IP без фолбэка, а http.Client.Timeout намеренно не задан (SSE) — запрос висел вечно. Три уровня защиты. (1) Транспорт (security.safeTransportFromGetter[TLS]— покрывает MCP, LLM-провайдеров, OAuth):ResponseHeaderTimeout=60s— ограничивает ожидание заголовков ответа (SSE-тела не тронуты: заголовки у стрима приходят сразу; весь LLM-трафик стриминговый, #222), не убирает «established-but-dead»-зависания; диалерdialAddrsFallback— последовательный фолбэк по всем IP, прошедшим SSRF-проверку, с общим баундом цикла 30s при ctx без дедлайна (N чёрных дыр не множат таймаут); пустой список адресов сохраняет stdlib-форму ошибки. (2) MCP (manager.go): каждая попытка коннекта ограничена (connectBounded: 30s сеть / 60s stdio, сеттер для тестов под RWMutex) — startup и reconnect health-loop больше не зависят навсегда от одного мёртвого эндпоинта; успех не конвертируется в ошибку при race с дедлайном; ошибка оборачивает исходную причину (Canceled различим от DeadlineExceeded). (3) Задокументирован агрегатный потолок stdio-ретраев (~4 мин на сервер) как follow-up. Митигация инцидента на хосте: фиксация рабочего A-рекорда в /etc/hosts. Тесты (+7): фолбэк диалера (мёртвый→живой, все мёртвые, пустой список), ResponseHeaderTimeout (тихий сервер ограничен, SSE-тело не обрезается), MCP Start (мёртвый эндпоинт не висит + помечается error, здоровый коннектится, bounded-ошибка различима).