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 или ежедневно ротировать с перезаписью — к тому времени, когда клиент сообщит о проблеме, исходная запись обычно уже уходит из видимой области;
  • Сообщать о проблеме по фото экрана с телефона — лучше вставьте текст; скриншоты регулярно обрезают половину строки ошибки.

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

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

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

Служба поддержки WeCom

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

Сопутствующая документация

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

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

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

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

Просмотр записей вызовов

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

Чтение сумм тарификации

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

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

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

Основы Image API

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