Как выглядит ошибка
Одна и та же первопричина проявляется двумя совершенно разными способами, в зависимости от того, с какой стороны вы смотрите.Со шлюза
Со стороны клиента
Сначала определите направление: кто разорвал соединение
Кодwrite_response_body_failed — ключевая подсказка: он означает шлюз не смог записать тело ответа обратно вызывающей стороне. Это направление downstream, а не ошибка upstream-модели. Результат уже был сгенерирован; соединение оборвалось в момент, когда его передавали вам.
Обрыв downstream
write_response_body_failed, connection reset by peer, SSL EOF на стороне клиента.
Соединение исчезло, пока шлюз передавал тело. Если платформа возвращает 500 в таком случае, это не тарифицируется — см. раздел о тарификации ниже.Сбой upstream (со стороны канала)
upstream_error, 5xx с полезной нагрузкой upstream или HTTP 200 с аномальным finishReason.
Это настоящие проблемы канала — передайте x-request-id в поддержку.Самый сильный сигнал: что журнал консоли говорит об этом вызове
Сначала проверьте журнал вызова в консоли, прежде чем делать что-либо еще. Это ничего не стоит и быстрее любого шага на стороне клиента сужает круг поиска:Четыре шага, чтобы снять с себя подозрение
Выполняйте их по порядку; большинство случаев решается уже на первых двух:Исключите неверное чтение на уровне приложения: вы могли получить его и не сохранить
gpt-image-2-all по умолчанию возвращает b64_json без префикса data:, поэтому код, который читает data[0].url, получает undefined, выбрасывает downstream, считается неудачным и запускает повторную попытку — за которую снова взимается плата.Симптомы идентичны сетевому сбою, но сетевой сбой здесь не требуется. Выведите одну строку для проверки:Проверьте, не сбоят ли все каналы и модели одновременно
Проверьте среду выполнения клиента: TLS stack (Python) или таймауты undici (Node.js)
Снизьте concurrency, перейдите на последовательный режим и отключите локальный proxy
Главный подозреваемый: TLS-стек клиента (особенно в macOS)
Python, который поставляется с macOS (/usr/bin/python3), использует LibreSSL 2.8.3, а не OpenSSL. В сочетании с urllib3 v2 такая связка надежно вызывает SSLEOFError при одновременной загрузке больших тел ответов. Клиент самопроизвольно разрывает соединение, а шлюз исправно фиксирует стену из connection reset by peer.
Одна команда для проверки
requests вы видите это предупреждение, это тот же сигнал:
Исправление: переключите интерпретатор
Не понижайте версию urllib3 — просто используйте Python, собранный с полноценным OpenSSL:Измеренное сравнение (2026-07-29, UTC+8)
Реальные цифры из сравнительного теста на двух каналах серии Nano Banana (gemini-3-pro-image / gemini-3.1-flash-image):
Что проверять на Linux-сервере (совершенно другой список)
1. Таймауты бездействия на облачных NAT-шлюзах и балансировщиках нагрузки (главная причина на стороне сервера)
Это главный источникconnection reset by peer в боевой среде. Возьмите AWS NAT Gateway: он использует фиксированный, не настраиваемый таймаут бездействия в 350 секунд, и когда он срабатывает, он отправляет RST, а не FIN — так что клиент видит ровно ECONNRESET.
Плохая часть в том, что это каскадируется: как только соединения из пула простаивали дольше 350 секунд, они все мертвы, поэтому ваш запрос получает RST на первом, клиент незаметно повторяет попытку на следующем соединении из пула — которое было столь же простаивающим и тоже получает RST. Типичный признак — «тихо какое-то время, потом несколько вызовов подряд падают, затем все снова нормально».
Исправления (любое из них; первые два предпочтительнее):
- Установите TCP keepalive ниже 350 секунд, чтобы трафик шел даже в тихие периоды;
- Ограничьте, как долго соединения могут простаивать в пуле, чтобы потенциально мертвые отбрасывались (Node:
new Agent({ keepAliveTimeout: 60_000 }); Pythonrequests: настройте пул черезHTTPAdapter); - Обходите NAT-шлюз полностью, например через VPC endpoints.
2. Значение TCP keepalive по умолчанию фактически «выключено»
В Linux значение по умолчаниюtcp_keepalive_time 7200 секунд (2 часа), что намного дольше любых таймаутов бездействия выше, поэтому на практике это бесполезно:
3. MTU контейнерной сети
Overlay-сети Docker / Kubernetes (flannel VXLAN и им подобные) обычно работают с MTU 1450, а не 1500. Добавьте к этому PMTUD black hole на пути — и получите классический случай «маленькие запросы всегда проходят, большие ответы всегда зависают»:4. Ограничения памяти контейнера → процесс получает OOMKilled
Один 4K base64 payload может достигать 20-30 MB; если загружать его целиком сresp.json() при параллельных запросах, легко превысить лимит памяти контейнера, и ядро убьет процесс — что снова выглядит как «соединение просто оборвалось»:
5. Переменные среды proxy (самая коварная на серверах)
На серверах часто глобально заданы значенияHTTP_PROXY / HTTPS_PROXY / NO_PROXY в /etc/environment, systemd unit или Dockerfile — и тот, кто их задавал, давно о них забыл. Хуже того, языки по-разному относятся к их учету:
api.apiyi.com в NO_PROXY — или просто никаких переменных proxy вообще.
Разовая самопроверка на стороне сервера
Node.js: три независимых тайм-аута, до которых SDK timeout не может дотянуться
Встроенный fetch в Node 18+ работает на undici, который имеет три независимых тайм-аута, покрывающих три этапа запроса. «Но я выставил тайм-аут на 5 минут» обычно означает, что вы изменили четвёртое значение, которое не относится ни к одному из них:
Правильная настройка
Для увеличения трёх тайм-аутов undici требуетсяAgent, либо глобально, либо для каждого запроса:
maxRetries по умолчанию равен 2 и автоматически повторяет ошибки подключения
openai-node по умолчанию использует maxRetries: 2, и как ошибки подключения, так и тайм-ауты входят в область действия этих автоматических повторов. Один логический вызов поэтому может породить три фактических запроса даже тогда, когда в вашем коде вообще нет логики повторов (будет ли каждый из них тарифицироваться, зависит от того, в какую категорию «Влияние на тарификацию» он попадет).
Эндпоинты для изображения — это дорогие синхронные долгие запросы, поэтому всегда задавайте maxRetries: 0 явно и берите логику повторов на себя, со своим backoff и своим пределом попыток. Правила тарификации описаны в Стратегия повторов.
повторное использование keep-alive уже мертвого соединения
undici по умолчанию включает пул соединений с keep-alive. Когда VPN, NAT или proxy незаметно забирает неактивное соединение, клиент этого не узнает и все равно берет это соединение из пула для следующего запроса — запись немедленно получает RST, что проявляется какread ECONNRESET.
Это самый частый источник ECONNRESET, когда вызовы идут с паузами, и это объясняет и «ошибки группируются в одном временном окне», и «даже первый повторный запрос завершается неудачей». Проверьте это, отключив повторное использование:
Локальный прокси / VPN: первое слабое место, которое выявляют эндпоинты генерации изображений
fake-ip / промах правила маршрутизации
198.18.x.x, вызывая строго 10-секундный таймаут подключения. Заметьте, это не «медленно подключается» — маршрута вообще нет, поэтому увеличение connect.timeout не поможет. Всегда фиксируйте remote_ip, до которого вы фактически дошли.Перехвачен как неактивное соединение во время генерации
MTU / чёрная дыра PMTUD
Расшифровка MITM плюс полная буферизация
Content-Length и ошибаются с длиной, из-за чего возникает RST. И снова это затрагивает только MB-объемные ответы с изображениями, никогда не текстовые вызовы.Диагностическая матрица
Это основа раздела. Есть только две оси: сбой произошел до или после первого байта, и сколько байт пришло.Одна команда для полного временного профиля
connect → стадия соединения; нет ttfb и круглый total → перехват как неактивного; полный bytes, но total ≈ ttfb + 300 плюс curl: (18) → отсутствует терминатор; bytes застыл на десятках KB → MTU.
Other common triggers
Ручное прерывание во время выполнения
write_response_body_failed след на шлюзе. Это ложная тревога, которую чаще всего принимают за «канал нестабилен».Срабатывает внешний таймаут первым
Пул соединений и чрезмерные параллельные запросы
Тело ответа исчерпывает память
resp.json() при параллельных запросах, это может исчерпать память контейнера и привести к завершению процесса из-за OOM — что снова выглядит как «соединение оборвалось без причины».Влияние на тарификацию: какие сбои стоят денег, а какие нет
Эти два сценария постоянно смешивают, хотя тарифицируются они противоположным образом:Шлюз возвращает 500 write_response_body_failed — не тарифицируется
Эта ошибка означает, что соединение оборвалось в тот момент, когда шлюз записывал обратно вам данные изображения. Платформа автоматически повторяет попытку внутри системы 2-3 раза, и только после неудачи всех попыток возвращает 500.В этом случае списание не происходит. Даже если такие ошибки идут подряд в одном и том же запросе, в вашем счете не появляется соответствующая строка — вы никогда не платите за эти сбои.write_response_body_failed.
Политику повторных попыток следует выдерживать соответствующе: сбои на уровне транспорта имеет смысл повторять, но каждая повторная попытка может быть отдельно тарифицируемым вызовом (в зависимости от того, к какой из двух категорий она попадет). Никогда не пишите бесконечный цикл повторных попыток.
Как правильно повторять попытку
Основное правило: повторяйте только исключения на уровне транспорта, никогда не повторяйте ошибки на уровне HTTP. Повторённый десять тысяч раз 4xx по-прежнему остаётся 4xx, и это лишь впустую тратит время.Передавайте response потоком вместо полной загрузки
Для больших тел читайте фрагмент за фрагментом с помощьюstream=True. Это снижает пиковое потребление памяти и показывает вам точно, на каком этапе передачи произошел сбой:
Когда действительно стоит обращаться в поддержку
Когда вы уже исключили локальные причины, эскалируйте, если выполняется хотя бы одно из следующих условий:- Это по-прежнему воспроизводится стабильно после перехода на корректный интерпретатор OpenSSL и снижения до последовательного выполнения;
- Сбой наблюдается только у одного конкретного канала или модели, тогда как остальные в том же окне работают нормально;
- Тело ответа пришло полностью (количество байт совпадает с
Content-Length), но соединение так и не закрывается, пока не истечет таймаут — это означает отсутствие завершающего chunked-терминатора upstream, проблема на стороне канала; - Ошибка однозначно направлена на upstream (
upstream_error, raw upstream 5xx).
x-request-id, время вызова (с часовым поясом, например 2026-07-29 14:32 (UTC+8)), имя модели, ключевые параметры, такие как imageSize, необработанное исключение клиента и шаги самопроверки, которые вы уже выполнили.