[bug][ai-chat] z.ai (GLM-5.2 coding plan) intermittently stalls/RSTs the chat stream (UND_ERR_HEADERS_TIMEOUT / ECONNRESET) #140
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Симптом
AI-чат к настроенному провайдеру (z.ai GLM-5.2, OpenAI-compatible, Base URL
https://api.z.ai/api/coding/paas/v4→/chat/completions) интермиттентно виснет на «Thinking…» и затем падает с баннером «Lost connection to the AI provider» / «Lost connection to the server».AiChatService/aiFetchк z.ai).Ключевое отличие: тест проходит, чат — нет
generateText({ prompt: 'ping', maxOutputTokens: 16 })—ai.service.ts:testConnectionstreamText({...})—ai-chat.service.ts:stream<- 200 in ~2000msМодель/транспорт/эндпоинт у обоих идентичны (
getChatModel→createOpenAI({ baseURL, fetch: aiFetch }).chat(model)). Разница — стриминг + размер/состав запроса.Доказанные факты (из серверных логов)
Наблюдаемые режимы падения (интермиттентно, на одном и том же запросе):
z.ai не присылает заголовки ответа >300с, и наш undici
headersTimeout(дефолт 300_000ms) обрывает запрос. Провайдер при этом соединение НЕ рвёт.z.ai сбрасывает соединение через ~2–9с.
provider request <- 200 in 1974ms / 8310ms (headers received).Вывод: coding-эндпоинт z.ai (
api/coding/paas/v4) ведёт себя нестабильно — то отвечает быстро, то застревает без заголовков (дольше нашего таймаута), то рвёт соединение. Тривиальныйpingпроскакивает, т.к. отвечает за <2с.Гипотезы, что проверили, и результат
Safari-специфика (idle SSE /
Connection: keep-aliveна HTTP/2) → коммит1b4de2b4(server-side SSE heartbeat: pingкаждые 15с + снятие hop-by-hopConnection/Keep-Alive).Результат: SSE-устойчивость браузер↔сервер улучшена и сам по себе фикс корректен, но корень не тут — виснет и в Chrome. Оставлено как полезная защита, проблему не лечит.
Кастомный транспорт undici (
aiFetch/RetryAgent), добавленный в1af5d34a→ проверено байпасом: коммит7c308728ввёл env-флагAI_BYPASS_RESILIENT_FETCH=true, который уводит чат-модель на дефолтный глобальныйfetch(без RetryAgent).Результат: с байпасом всё равно падает (тот же ECONNRESET/headers timeout) → транспорт ни при чём.
Гонка генерации заголовка со стримом. Для нового чата
generateTitle(generateText) запускался параллельно соstreamTextк тому же z.ai. Лог показывал два одновременныхprovider requestна один ход: один<- 200 in 8310ms, другойx after 301209ms(завис). Coding-план z.ai стопорит один из двух параллельных запросов.Фикс: коммит
fd66ee6c—generateTitleперенесён из «перед pipe» вonFinish(уходит соло, после завершения стрима).Результат: параллельный запрос убран (подтверждено логом — теперь один
getChatModel/provider requestна ход), 300с-гонка ушла, но соло-стрим всё равно падает (ECONNRESET ~2с либо headers timeout 300с). Фикс правильный (гонку убрал), но это была не единственная причина.Наш
headersTimeout(300с) слишком короткий. Развёрнутый лог причины (коммитb7abb7ea—aiFetchлогирует цепочкуerr.causeс кодами) дал решающее[UND_ERR_HEADERS_TIMEOUT]. Пробовали поднятьheadersTimeout/bodyTimeoutдо 600с (env-configurable) — откатили (не закоммичено): владелец считает, что не поможет (z.ai в режиме «застрял» не отвечает и в большем окне; это лишь митигация, не корень).Диагностика, добавленная в код (на
develop)7c308728— тайминговые логи жизненного цикла хода:AI chat stream START(chatId + UA),first chunk (...) after Nms,FINISHED in Nms, длительности вonError/onAbort/disconnect. Плюс env-флагAI_BYPASS_RESILIENT_FETCH.b7abb7ea—aiFetchпишетprovider request #N -> METHOD host/path,<- status in Nms (headers received)и при падении цепочку причинfetch failed <- [CODE] message(chat-вызовы на info, embeddings на debug; секреты не логируются — только host+path).Смотреть:
docker logs <container> 2>&1 | grep -iE "AiHttp|AI chat stream".Текущий вывод
Корень — на стороне z.ai coding-эндпоинта (
api/coding/paas/v4): он интермиттентно не отдаёт заголовки ответа и/или рвёт соединение на «тяжёлый» стриминговый запрос (reasoning + инстр��менты + большой контекст), тогда как тривиальныйpingпроходит. Наш код послеfd66ee6cшлёт один корректный запрос на ход; обрыв происходит внутри обращения к z.ai.Что проверить на стенде (открытые вопросы)
tcpdump/прокси/curl -Nс тем же телом запроса) и посмотреть, когда приходит статус-строка200и первый SSE-чанк.coding/paas)?tools) и/или инъекцию текущего документа и проверить, проходит ли тогда стрим.headersTimeout(т.е. z.ai отвечает в большем окне) или он зависает намертво — повторить с временно поднятым таймаутом и посмотреть, доходит ли до<- 200.<- 200 in 8310ms) и зависшие ходы: что отличает запрос (длина промпта, число tool-спеков, размер документа)?Затронутые коммиты / файлы (
develop)Коммиты:
1af5d34a— fix(ai-chat): reconnect on provider ECONNRESET via a resilient fetch (ввёлaiFetch/RetryAgent).1b4de2b4— fix(ai-chat): keep SSE stream alive in Safari (heartbeat + strip hop-by-hop Connection).7c308728— chore(ai-chat): add stream timing logs + env-gated aiFetch bypass (diagnostics).fd66ee6c— fix(ai-chat): stop title generation racing the chat stream (provider stall).b7abb7ea— feat(ai-http): log detailed fetch error cause chain.Файлы:
apps/server/src/integrations/ai/ai-http.ts—aiFetch,baseAgent(connect.timeout:10s,keepAliveTimeout:4s, undici defaultheadersTimeout/bodyTimeout300s),RetryAgent(maxRetries:2, errorCodes безUND_ERR_HEADERS_TIMEOUT).apps/server/src/integrations/ai/ai.service.ts—getChatModel(+ envAI_BYPASS_RESILIENT_FETCH),testConnection.apps/server/src/core/ai-chat/ai-chat.service.ts—stream()/streamText(onChunk/onStepFinish/onFinish/onError/onAbort),generateTitle.apps/server/src/core/ai-chat/ai-chat.controller.ts— POST/api/ai-chat/stream,res.hijack(),onClose(abort при дисконнекте клиента).Доступные env-рычаги (в коде сейчас)
AI_BYPASS_RESILIENT_FETCH=true— чат-модель идёт через дефолтныйfetch(безRetryAgent). Для изоляции транспорта.AI_HTTP_HEADERS_TIMEOUT_MS/AI_HTTP_BODY_TIMEOUT_MS— пробовали, откатили, в коде сейчас нет.)Контекст: с GLM и инстурментами все работало, но иногда ломалось (там есть коммиты где мы чинили транспорт на беке и восстановление переписки на фронте). Потом сломалось совсем. Бекенд ллм живой, курлом проверяется. Делаю вывод что это регрессия в одном из коммтов. Предлагаю бисектом найти и починить.
Ghost referenced this issue2026-06-24 05:28:16 +03:00