Skip to main content

Сначала главное: сырая ошибка появляется ровно один раз — в теле ответа

APIYI возвращает сведения об ошибке только через тело ответа API. Лог backend — это журнал тарификации: он фиксирует вызовы, за которые была начислена тарификация. Неудачные запросы ни не тарифицируются, ни не попадают туда.Так что «не могу найти это в backend-логе» не означает «этого не было». Это значит, что единственная запись об этой ошибке находится у вас в клиенте. Если вы не вывели ее и не сохранили, она потеряна навсегда — и мы тоже не сможем ее восстановить.
Краткий вывод: печатайте сырое тело ответа в точности так, как оно возвращено; не храните только однострочную строку, в которую его завернула ваша программа. Строка вроде 400 Bad Request почти ничего не дает для диагностики — настоящий ответ был в JSON, который она отбросила.

Реальный случай: 400 Bad Request ничего вам не говорит

Клиент сообщил ровно одну строку:
Эта строка — то, что клиентский фреймворк выдал после обработки ошибки. Он сохранил имя модели, HTTP-метод, URL и код состояния — и отбросил единственную важную вещь, тело ответа. Лучший ответ, который могла дать поддержка, был таким:
400 обычно означает либо проверку безопасности контента, либо проблему с параметром. Скорее всего, проверку безопасности контента.
Это предположение, а не вывод. Потому что для того же самого вызова фактическое тело ответа могло быть любым из следующих трех вариантов — и каждый из них требует совершенно разного действия:
Один и тот же 400, три совершенно разных действия. Игнорирование тела ответа превращает выбор из трех вариантов в угадайку — и неверное предположение стоит вам лишнего обмена сообщениями с поддержкой плюс ненужной повторной попытки.Хуже того: две из этих трех ситуаций вообще не следует повторять. Если вы не можете их различить, слепые повторные попытки — ваш единственный вариант, и они тратят и время, и квоту.

Что есть и чего нет в backend log

Сначала важно правильно понять ментальную модель: backend log — это журнал тарификации, а не журнал ошибок.
Если читать это в обратном порядке, это становится самым сильным одиночным тестом на проблемы с соединением: если журнал содержит запись о тарификации, запрос дошел до upstream и потребил ресурсы; если не содержит, проблема почти наверняка возникла до достижения upstream (сеть, аутентификация, проверка параметров). Полные подробности в Чтении списанных сумм в журнале.

7 полей, которые вы должны сохранить

Это все, что нужно, чтобы диагностировать один неудачный вызов. Если не хватает хотя бы одного из них, диагностика снова превращается в догадки:
Не обрезайте тело ответа. Обрезать до 200 символов разумно для обычных бизнес-логов, но для диагностики полезные детали часто находятся в конце. Сохраняйте как минимум первые 2000 символов. Если вас беспокоит, что base64 заспамит логи на эндпоинтах генерации изображений, выводите полное тело только когда status_code >= 400 — тела ошибок и так короткие.

Правильные шаблоны захвата ошибок

По сути, есть только один принцип: перехватывайте на двух уровнях и ничего не теряйте ни на одном из них.
  • Сбои на транспортном уровне: сброс соединения, сбой TLS-рукопожатия, тайм-аут, сбой DNS. Ответа HTTP вообще нет — у вас есть только текст исключения.
  • Ошибки на уровне HTTP: сервер вернул 4xx / 5xx. Ответ есть, и вы должны прочитать его тело.

Python / requests

Не вызывайте raise_for_status() до чтения тела. Исключение HTTPError, которое оно выбрасывает, содержит только 400 Client Error: Bad Request for url: ..., тогда как настоящее сообщение лежит нетронутым в resp.text, и его никто не читает — именно так и происходит случай, описанный в начале этой страницы. Если вы все же используете его, сначала извлеките resp.text.

Python / OpenAI SDK

Официальный SDK уже прикрепляет к объекту исключения все три части. Большинство людей вместо этого просто print собственное однострочное сообщение:
Даже однострочное сообщение должно быть print(f"API error: {e}"), а не print("request failed")str(e) исключения SDK уже содержит сообщение сервера. На самом деле информация теряется, когда вы полностью выбрасываете объект исключения.

Node.js

С SDK:
С обычным fetch чаще всего ошибка возникает именно здесь:
Тот самый 400 Bad Request from POST https://api.apiyi.com/v1/images/edits в начале этой страницы буквально ${resp.status} ${resp.statusText} from ${resp.method} ${resp.url}тело вообще ни разу не было прочитано.fetch не отклоняет запросы на уровне HTTP-ошибок; оно просто устанавливает resp.ok в false. Если в этот момент выбросить resp.statusText, тело будет отброшено вместе с объектом ответа. Всегда await resp.text(), прежде чем выбрасывать исключение — одна эта строка отделяет диагностируемый отчет от неразрешимого.

Воспроизведение с помощью cURL

Когда вы просите кого-то воспроизвести проблему, эта команда требует меньше всего усилий — она одним запуском показывает код состояния, заголовки, тело и время выполнения:
  • -i выводит заголовки ответа, где и находится x-request-id;
  • -sS скрывает индикатор выполнения, но сохраняет вывод ошибок;
  • -w добавляет код состояния и общее время, что удобно для сравнения с вашими настройками тайм-аута.

Обертки и внутренние шлюзы

Хороший пример

Эта ошибка пришла из узла ComfyUI клиента:
Это куда уродливее, чем 400 Bad Request — и при этом оно полное, поэтому направление определяется за секунды: Вывод следует немедленно: это проблема транспортного уровня, не связанная с безопасностью контента или параметрами, и она не приводит к списанию (запрос так и не был завершен). Путь диагностики: Обрывы соединения с Image API.
Сравните два варианта: один аккуратно упакован и ничего не объясняет (400 Bad Request); другой длинный и уродливый и прямо указывает на коренную причину (errno 10054). Для диагностики сырая, уродливая и полная ошибка всегда лучше дружелюбной, аккуратной, переписанной версии.

Где найти сырой вывод в распространенных инструментах

Три правила для внутреннего шлюза

1

Не переписывайте, а только пропускайте дальше

Промежуточный слой может добавлять контекст (какой сервис, какой tenant, какая попытка повтора), но не должен заменять вышестоящий error.message. После переписывания больше негде восстановить исходный текст.
2

Храните сообщение для пользователя отдельно от исходного

Следуйте трехчастной структуре, использованной в обработке ошибок генерации изображений Gemini: userMessage (дружелюбный текст для конечных пользователей), devMessage (ваша классификация для разработчиков) и rawResponse (тело ответа без изменений). Первые два поля можно свободно полировать; третье храните дословно.
3

Никогда не выдавайте неизвестную ошибку

В резервной ветке запишите status, x-request-id и первые 2000 символов тела. «Неклассифицированная ошибка», которая сохраняет исходный текст, поддается диагностике; чистая «неизвестная ошибка» — нет.

Антипаттерны, делающие диагностику невозможной

  • except Exception as e: print("request failed") — объект исключения исчез, и вы даже не знаете, какой слой отказал;
  • Записывать код состояния, но не тело — именно тот случай, что вверху этой страницы;
  • Вызывать raise_for_status(), не прочитав сначала resp.text, — сообщение все еще находится в памяти, просто никогда не извлекается;
  • if (!resp.ok) throw new Error(resp.statusText) в fetch — тело отбрасывается вместе с объектом ответа;
  • Оставлять только чистый 200 после успешной повторной попытки — регистрируйте каждую попытку отдельно, иначе вы никогда не увидите, сколько раз отказал транспорт, и можете принять собственные повторы за поведение шлюза;
  • Логировать только в stdout или ежедневно ротировать с перезаписью — к тому времени, когда клиент сообщит о проблеме, исходная запись обычно уже уходит из видимой области;
  • Сообщать о проблеме по фото экрана с телефона — лучше вставьте текст; скриншоты регулярно обрезают половину строки ошибки.

Когда обращаться в поддержку

Сначала пройдите описанные выше шаги захвата и интерпретации. Если выполняется хотя бы одно из следующих условий, передайте материалы в поддержку:
  • У вас есть полное тело ответа, и error.message указывает на upstream (upstream_error, необработанный upstream 5xx или явная ошибка канала);
  • Те же параметры запроса работают на другой модели или в другое время, и стабильно сбой происходит только у одной конкретной модели;
  • Ошибка — это 500 + write_response_body_failed или похожий сбой доставки downstream, и она воспроизводится стабильно (за это не тарифицируются; см. Обрывы соединения);
  • Вы подозреваете, что тарификация не совпадает с вашими фактическими вызовами — только request_id позволяет точно сверить данные.

Шаблон обращения в поддержку (копируйте и вставляйте)

Поддержка WeCom

QR-код поддержки WeComОтсканируйте код или нажмите эту карточку, чтобы связаться со службой поддержки WeCom.Также вы можете связаться с нами в Telegram по @apiyi001 или по email [email protected].
Отправьте шаблон выше в виде текста — это намного эффективнее любого описания. С request_id мы сможем сразу перейти к полной трассировке этого единственного вызова, вместо того чтобы спрашивать: «примерно когда и какая модель?» См. API запроса логов, чтобы узнать, как найти request_id.

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

Руководство по API

Распространенные коды ошибок, аутентификация и лимиты запросов

Обрывы соединения

Полный путь устранения неполадок для ECONNRESET, errno 10054 и SSL EOF

API запроса логов

Получайте журналы вызовов через API — как искать request_id и сверять тарификацию

Чтение начисленных сумм

Почему неудачные вызовы никогда не попадают в лог и как использовать это как диагностический тест

Настройка таймаута

Уровни таймаута по типам моделей и что проверять, если его увеличение не помогает

Основы Image API

Синхронные вызовы, различия префиксов base64, предварительная обработка 400 invalid_image_file