Skip to main content

Сначала тарификация: write_response_body_failed 500 не тарифицируется

Когда шлюз возвращает 500 с write_response_body_failed / connection reset by peer, платформа уже автоматически повторила попытку 2-3 раза и показывает ошибку только после того, как все попытки завершились неудачей. За эти запросы не взимается никакая плата.Так что даже если в ваших логах накапливаются такие ошибки, в вашем счете не появится соответствующее списание — вы не платите за сбои. Есть другой случай, который действительно тарифицируется (когда ваш клиент уходит слишком рано); см. раздел «Влияние на тарификацию» ниже.
Краткий ответ: ответы API для image generation обычно имеют размер от десяти до нескольких десятков МБ, и сбой почти всегда происходит во время загрузки ответа (не из-за того, что тело запроса слишком большое — обычный text-to-image тоже дает сбой). Сначала проверьте, к какой категории относится лог консоли, затем выполните приведенные ниже самопроверки для macOS / Linux / Node.js.

Как выглядит ошибка

Одна и та же первопричина проявляется двумя совершенно разными способами, в зависимости от того, с какой стороны вы смотрите.

Со шлюза

Со стороны клиента

На Node.js различайте «killed» и «closed»: ECONNRESET означает, что пришел TCP RST — соединение было killed чем-то на пути, что указывает на сетевой переход. SocketError: other side closed / ERR_STREAM_PREMATURE_CLOSE означает, что удаленная сторона закрыла соединение штатно (FIN), что указывает на проблему с финализацией на стороне сервера, например на отсутствующий chunked terminator. Эти два варианта указывают в совершенно разные стороны; не рассматривайте их как один и тот же симптом.Также, UND_ERR_* может исходить только из undici (движка, стоящего за встроенным fetch в Node 18+), тогда как read ECONNRESET — это верхнеуровневая формулировка libuv, которую также выдают axios / node-fetch / модуль http. Если оба типа появляются вместе, сначала проверьте, есть ли в вашем приложении два разных HTTP пути — в таком случае это вообще не один и тот же инцидент.

Сначала определите направление: кто разорвал соединение

Код write_response_body_failed — ключевая подсказка: он означает шлюз не смог записать тело ответа обратно вызывающей стороне. Это направление downstream, а не ошибка upstream-модели. Результат уже был сгенерирован; соединение оборвалось в момент, когда его передавали вам.
Это не вызвано слишком большим телом запроса. Загрузки при редактировании изображений используют референсные изображения, из-за чего возникает подозрение на размер загрузки — но обычный text-to-image, у которого тело запроса всего несколько сотен байт, ломается так же часто. Сбой происходит при загрузке ответа: payload изображений имеют размер от десяти до нескольких десятков MB и являются самым хрупким звеном всего пути.

Обрыв downstream

write_response_body_failed, connection reset by peer, SSL EOF на стороне клиента. Соединение исчезло, пока шлюз передавал тело. Если платформа возвращает 500 в таком случае, это не тарифицируется — см. раздел о тарификации ниже.

Сбой upstream (со стороны канала)

Таймауты upstream, upstream_error, 5xx с полезной нагрузкой upstream или HTTP 200 с аномальным finishReason. Это настоящие проблемы канала — передайте x-request-id в поддержку.

Самый сильный сигнал: что журнал консоли говорит об этом вызове

Сначала проверьте журнал вызова в консоли, прежде чем делать что-либо еще. Это ничего не стоит и быстрее любого шага на стороне клиента сужает круг поиска:
Это приводит к выводу, который люди часто понимают наоборот: сбои на этапе соединения, такие как UND_ERR_CONNECT_TIMEOUT, не могут привести к списанию, потому что запрос никогда не достигал шлюза. Так что если вы видите «много connect timeouts» и «много списаний», это точно не одни и те же запросы — разбирайте их отдельно, а не заставляйте одну причину объяснять все.

Четыре шага, чтобы снять с себя подозрение

Выполняйте их по порядку; большинство случаев решается уже на первых двух:
1

Исключите неверное чтение на уровне приложения: вы могли получить его и не сохранить

«Ответа не было» обычно оказывается выводом после того, как код выбросил исключение и был пойман как «request failed» — а не доказательством, что байты вообще не пришли. Самый частый случай: gpt-image-2-all по умолчанию возвращает b64_json без префикса data:, поэтому код, который читает data[0].url, получает undefined, выбрасывает downstream, считается неудачным и запускает повторную попытку — за которую снова взимается плата.Симптомы идентичны сетевому сбою, но сетевой сбой здесь не требуется. Выведите одну строку для проверки:
Различия полей и префиксов по сериям приведены в справочнике префикса base64.
2

Проверьте, не сбоят ли все каналы и модели одновременно

Если два разных канала, работающих на двух разных моделях, в один и тот же временной интервал выдают одну и ту же ошибку, причины, специфичные для канала, по сути исключены — upstream не отказывают так синхронно.
3

Проверьте среду выполнения клиента: TLS stack (Python) или таймауты undici (Node.js)

Python: проверьте версию TLS stack. Node.js: проверьте три таймаута undici. Оба случая разобраны в следующих двух разделах. Это самая частая первопричина на практике, она полностью находится на вашей машине, и одна команда это подтверждает.
4

Снизьте concurrency, перейдите на последовательный режим и отключите локальный proxy

Повторно запустите те же запросы при concurrency 1-2 с отключенными VPN или proxy. Если в таком режиме ошибка ни разу не воспроизводится, проблема в обработке соединений клиентом, локальных ресурсах (connection pool, file descriptors, memory) или сетевом пути — не в канале.

Главный подозреваемый: 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-сервере (совершенно другой список)

Проверка TLS-stack выше проходит практически на любой Linux-машине — Python из дистрибутива линкуется с обычным OpenSSL, так что ловушки LibreSSL там нет. Не останавливайтесь на этой проверке. Проблемы на стороне сервера живут в исходящем пути и в ограничениях контейнера, а это уже совсем другой мир по сравнению с локальной разработкой.

1. Таймауты бездействия на облачных NAT-шлюзах и балансировщиках нагрузки (главная причина на стороне сервера)

Это главный источник connection reset by peer в боевой среде. Возьмите AWS NAT Gateway: он использует фиксированный, не настраиваемый таймаут бездействия в 350 секунд, и когда он срабатывает, он отправляет RST, а не FIN — так что клиент видит ровно ECONNRESET. Плохая часть в том, что это каскадируется: как только соединения из пула простаивали дольше 350 секунд, они все мертвы, поэтому ваш запрос получает RST на первом, клиент незаметно повторяет попытку на следующем соединении из пула — которое было столь же простаивающим и тоже получает RST. Типичный признак — «тихо какое-то время, потом несколько вызовов подряд падают, затем все снова нормально».
Это тот же механизм, что и случай «keep-alive повторно использует мертвое соединение» в разделе Node.js — на сервере виновником обычно является NAT-шлюз облачного провайдера, а не локальное прокси-ПО.
Исправления (любое из них; первые два предпочтительнее):
  • Установите TCP keepalive ниже 350 секунд, чтобы трафик шел даже в тихие периоды;
  • Ограничьте, как долго соединения могут простаивать в пуле, чтобы потенциально мертвые отбрасывались (Node: new Agent({ keepAliveTimeout: 60_000 }); Python requests: настройте пул через HTTPAdapter);
  • Обходите NAT-шлюз полностью, например через VPC endpoints.
Другие облака и self-hosted балансировщики нагрузки используют другие значения бездействия, но подход одинаков: найдите самый короткий таймаут бездействия на пути и установите keepalive ниже него.

2. Значение TCP keepalive по умолчанию фактически «выключено»

В Linux значение по умолчанию tcp_keepalive_time 7200 секунд (2 часа), что намного дольше любых таймаутов бездействия выше, поэтому на практике это бесполезно:
Включать keepalive непосредственно в HTTP client — более надежный путь, поскольку контейнеры часто не могут менять параметры ядра.

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 — и тот, кто их задавал, давно о них забыл. Хуже того, языки по-разному относятся к их учету: Это несоответствие дает по-настоящему запутанные результаты: на одной машине curl и Python ходят через прокси, а Node подключается напрямую (или наоборот), поэтому они расходятся, и диагностика приводит к противоречивым выводам. Сначала проверьте:
APIYI доступен напрямую, так что на сервере обычно вам нужен api.apiyi.com в NO_PROXY — или просто никаких переменных proxy вообще.

Разовая самопроверка на стороне сервера

Node.js: три независимых тайм-аута, до которых SDK timeout не может дотянуться

Встроенный fetch в Node 18+ работает на undici, который имеет три независимых тайм-аута, покрывающих три этапа запроса. «Но я выставил тайм-аут на 5 минут» обычно означает, что вы изменили четвёртое значение, которое не относится ни к одному из них:
Параметр timeout в openai-node — это тайм-аут всего запроса на основе AbortController, и он не распространяется ни на один из трёх тайм-аутов выше. Увеличение timeout с 60 до 300 секунд оставляет connectTimeout на уровне 10 секунд. То же самое верно и для AbortSignal.timeout() в чистом fetch().Это самая частая причина «мой тайм-аут огромный, но он все равно срабатывает» — был изменён не тот уровень.

Правильная настройка

Для увеличения трёх тайм-аутов undici требуется Agent, либо глобально, либо для каждого запроса:

maxRetries по умолчанию равен 2 и автоматически повторяет ошибки подключения

openai-node по умолчанию использует maxRetries: 2, и как ошибки подключения, так и тайм-ауты входят в область действия этих автоматических повторов. Один логический вызов поэтому может породить три фактических запроса даже тогда, когда в вашем коде вообще нет логики повторов (будет ли каждый из них тарифицироваться, зависит от того, в какую категорию «Влияние на тарификацию» он попадет). Эндпоинты для изображения — это дорогие синхронные долгие запросы, поэтому всегда задавайте maxRetries: 0 явно и берите логику повторов на себя, со своим backoff и своим пределом попыток. Правила тарификации описаны в Стратегия повторов.
Сначала убедитесь, какой стек у вас на самом деле: node -v, npm ls openai undici axios node-fetch. Код UND_ERR_* лишь доказывает, что под капотом используется undici — это не доказывает, что вы используете OpenAI SDK. Обычный fetch() выдаёт те же коды, а обычный fetch() вообще не имеет maxRetries.

повторное использование keep-alive уже мертвого соединения

undici по умолчанию включает пул соединений с keep-alive. Когда VPN, NAT или proxy незаметно забирает неактивное соединение, клиент этого не узнает и все равно берет это соединение из пула для следующего запроса — запись немедленно получает RST, что проявляется как read ECONNRESET. Это самый частый источник ECONNRESET, когда вызовы идут с паузами, и это объясняет и «ошибки группируются в одном временном окне», и «даже первый повторный запрос завершается неудачей». Проверьте это, отключив повторное использование:

Локальный прокси / VPN: первое слабое место, которое выявляют эндпоинты генерации изображений

APIYI напрямую доступен внутри материкового Китая и не требует прокси или VPN (см. Нужен ли мне прокси для использования API?). Поэтому выключить прокси и повторно проверить — это самый дешевый и самый информативный одиночный шаг, доступный вам.Но важно уточнить: прокси — лишь самый крупный подозреваемый фактор, а не установленная первопричина. Именно матрица ниже на самом деле локализует неисправность.
Два свойства делают эндпоинты генерации изображений намного чувствительнее, чем текстовые: 30-60 секунд, когда во время генерации не проходит ни одного байта, и тело размером в MB, передаваемое одним всплеском. Когда chat endpoints работают нормально, а эндпоинты генерации изображений падают, обычно виноват один из этих двух.

fake-ip / промах правила маршрутизации

В режиме fake-ip прокси промах правила направляет вас на недостижимый адрес вроде 198.18.x.x, вызывая строго 10-секундный таймаут подключения. Заметьте, это не «медленно подключается» — маршрута вообще нет, поэтому увеличение connect.timeout не поможет. Всегда фиксируйте remote_ip, до которого вы фактически дошли.

Перехвачен как неактивное соединение во время генерации

На протяжении 30-60 секунд после отправки запроса не проходит ни одного байта, и прокси перехватывает соединение по своей политике простоя. Характерный признак — время сбоя попадает в круглое число — 30 / 60 / 120 секунд — независимо от размера изображения.

MTU / чёрная дыра PMTUD

MTU туннеля ниже path MTU, а ICMP «требуется фрагментация» отбрасывается, из-за чего ломается PMTUD. Классический признак — малые запросы всегда проходят, крупные ответы всегда застревают, а полученные байты застывают на нескольких KB или нескольких десятках KB. Обычно помогает уменьшить MTU туннеля примерно до 1400.

Расшифровка MITM плюс полная буферизация

Прокси с включенной HTTPS-расшифровкой часто буферизуют крупные тела целиком и могут упереться в предел размера, либо переписывают chunked в Content-Length и ошибаются с длиной, из-за чего возникает RST. И снова это затрагивает только MB-объемные ответы с изображениями, никогда не текстовые вызовы.

Диагностическая матрица

Это основа раздела. Есть только две оси: сбой произошел до или после первого байта, и сколько байт пришло. Последняя строка — самый ценный одиночный тест: он уменьшает тело с нескольких MB примерно до 1 KB. Если режим URL стабильно проходит, а режим base64 стабильно падает, проблема масштабируется с объемом передачи, что сразу исключает первые два столбца.

Одна команда для полного временного профиля

Сверяйте его с матрицей: нет значения connect → стадия соединения; нет ttfb и круглый total → перехват как неактивного; полный bytes, но total ≈ ttfb + 300 плюс curl: (18) → отсутствует терминатор; bytes застыл на десятках KB → MTU.
При A/B-тестировании режима с прокси и без прокси чередуйте прогоны — никогда не объединяйте их в пачку. Пять прогонов через прокси подряд, а затем пять прямых прогонов позволяют ошибке временного окна исказить результат до совершенно неверного вывода; мы измеряли участки, где все ломалось, потом через несколько минут все работало, а потом снова ломалось. Запускайте proxy → direct → proxy → direct по одному, каждый раз записывая remote_ip.

Other common triggers

Ручное прерывание во время выполнения

Ctrl+C во время отладки, перезапуск процесса, hot reload, завершение работающего скрипта — каждый крупный ответ, который еще находится в пути, оставляет на write_response_body_failed след на шлюзе. Это ложная тревога, которую чаще всего принимают за «канал нестабилен».

Срабатывает внешний таймаут первым

Таймауты worker в очереди задач, лимиты выполнения Serverless, таймауты origin на шлюзе/CDN (обычно по умолчанию 60 секунд). Любой уровень, у которого таймаут меньше времени генерации, первым разрывает соединение — см. Обязательно к прочтению и лучшие практики.

Пул соединений и чрезмерные параллельные запросы

Пределы пула соединений, локальные лимиты file descriptor, NAT/межсетевой экран, незаметно освобождающие долгоживущие соединения. Большие ответы остаются открытыми гораздо дольше, поэтому они намного чаще упираются в эти лимиты, чем текстовые эндпоинты.

Тело ответа исчерпывает память

Один base64-блок размером 4K может достигать 20-30MB. Если загружать его целиком с resp.json() при параллельных запросах, это может исчерпать память контейнера и привести к завершению процесса из-за OOM — что снова выглядит как «соединение оборвалось без причины».

Влияние на тарификацию: какие сбои стоят денег, а какие нет

Эти два сценария постоянно смешивают, хотя тарифицируются они противоположным образом:

Шлюз возвращает 500 write_response_body_failedне тарифицируется

Эта ошибка означает, что соединение оборвалось в тот момент, когда шлюз записывал обратно вам данные изображения. Платформа автоматически повторяет попытку внутри системы 2-3 раза, и только после неудачи всех попыток возвращает 500.В этом случае списание не происходит. Даже если такие ошибки идут подряд в одном и том же запросе, в вашем счете не появляется соответствующая строка — вы никогда не платите за эти сбои.

Ваш клиент отключился слишком рано — тарифицируется как обычно

Другой случай — когда шлюз завершает доставку, а отключается первым ваша сторона: срабатывает таймаут клиента, во время отладки нажимается Ctrl+C, происходит перезапуск процесса или его принудительно завершает OOM.Генерация на сервере и в upstream уже завершилась, поэтому такие запросы тарифицируются как обычно — «я не получил изображение» не означает «с меня не списали». Многократная отправка крупных запросов на генерацию изображений во время отладки вполне может привести к вполне реальному счету.
Именно так их и различают в таблице выше: проверьте, что в консоли был зарегистрирован обычный вызов или 500 write_response_body_failed. Политику повторных попыток следует выдерживать соответствующе: сбои на уровне транспорта имеет смысл повторять, но каждая повторная попытка может быть отдельно тарифицируемым вызовом (в зависимости от того, к какой из двух категорий она попадет). Никогда не пишите бесконечный цикл повторных попыток.

Как правильно повторять попытку

Основное правило: повторяйте только исключения на уровне транспорта, никогда не повторяйте ошибки на уровне HTTP. Повторённый десять тысяч раз 4xx по-прежнему остаётся 4xx, и это лишь впустую тратит время.
Записывайте каждую попытку отдельно (список attempts выше). Иначе успешная повторная попытка клиента не оставит в журналах ничего, кроме чистого 200, и вы никогда не увидите, сколько раз на самом деле ломался транспорт. Эти данные необходимы при оценке качества канала, и они не дают вам неверно трактовать собственные повторные попытки как поведение канала.

Передавайте 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, необработанное исключение клиента и шаги самопроверки, которые вы уже выполнили.

Связанная документация

Обязательно к прочтению и лучшие практики

Синхронные вызовы, тайм-ауты для каждой модели, обработка base64, тарификация при разрыве соединения

Создайте собственную асинхронную очередь

Оборачивайте синхронные вызовы в очередь задач и компенсируйте редкие сбои повторными попытками

Нужен ли мне прокси?

APIYI подключается напрямую без прокси; выполняет самопроверку на проблемы с сертификатом и DNS

Обработка ошибок Gemini при генерации изображений

Коды ошибок и обработка finishReason для генерации изображений Gemini