> ## Documentation Index
> Fetch the complete documentation index at: https://docs.apiyi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Захват деталей ошибок API

> APIYI возвращает детали ошибок только в теле ответа API — журнал бэкенда является реестром тарификации и записывает только выставленные к оплате вызовы. На этой странице объясняется, почему вы должны самостоятельно выводить необработанную ошибку, приводятся корректные шаблоны захвата для Python / Node.js / cURL, способ получить необработанный вывод из обёрток вроде ComfyUI, 7 полей, которые нужно сохранить, и шаблон для обращения в поддержку, который можно скопировать и вставить.

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

  APIYI возвращает сведения об ошибке **только через тело ответа API**. Лог backend — это **журнал тарификации**: он фиксирует вызовы, за которые была начислена тарификация. Неудачные запросы ни не тарифицируются, ни не попадают туда.

  Так что «не могу найти это в backend-логе» не означает «этого не было». Это значит, что **единственная запись об этой ошибке находится у вас в клиенте**. Если вы не вывели ее и не сохранили, она потеряна навсегда — и мы тоже не сможем ее восстановить.
</Warning>

<Info>
  **Краткий вывод**: печатайте **сырое тело ответа** в точности так, как оно возвращено; не храните только однострочную строку, в которую его завернула ваша программа. Строка вроде `400 Bad Request` почти ничего не дает для диагностики — настоящий ответ был в JSON, который она отбросила.
</Info>

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

Клиент сообщил ровно одну строку:

```text theme={null}
apiyi GPT Image 2 2k: 400 Bad Request from POST https://api.apiyi.com/v1/images/edits
```

Эта строка — **то, что клиентский фреймворк выдал после обработки ошибки**. Он сохранил имя модели, HTTP-метод, URL и код состояния — и отбросил единственную важную вещь, **тело ответа**. Лучший ответ, который могла дать поддержка, был таким:

> 400 обычно означает либо проверку безопасности контента, либо проблему с параметром. Скорее всего, проверку безопасности контента.

Это **предположение**, а не вывод. Потому что для того же самого вызова фактическое тело ответа могло быть любым из следующих трех вариантов — и каждый из них требует совершенно разного действия:

| `error.message` в теле ответа                    | Реальная причина                                                                                                                                | Что следует сделать                                                                                                                                           |
| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Your request was rejected by the safety system` | Блокировка проверки безопасности контента на стороне upstream                                                                                   | Измените prompt или референсное изображение. **Не повторяйте попытку** — оно снова будет заблокировано. См. [Безопасность контента](/ru/faq/content-safety)   |
| `Invalid value for 'size': expected one of ...`  | Недопустимое значение enum в параметре                                                                                                          | Ошибка на уровне кода. Исправьте параметр; повторять бессмысленно                                                                                             |
| `invalid_image_file` / `Invalid input image`     | Само референсное изображение недействительно (например, `.jpg` с некоторых Android-смартфонов на самом деле является много-кадровым MPO-файлом) | Перекодируйте его с помощью Pillow или аналогичного инструмента, затем отправьте снова. См. [Основы Image API](/ru/api-capabilities/image-api-best-practices) |

<Warning>
  **Один и тот же 400, три совершенно разных действия.** Игнорирование тела ответа превращает выбор из трех вариантов в угадайку — и неверное предположение стоит вам лишнего обмена сообщениями с поддержкой плюс ненужной повторной попытки.

  Хуже того: **две из этих трех ситуаций вообще не следует повторять.** Если вы не можете их различить, слепые повторные попытки — ваш единственный вариант, и они тратят и время, и квоту.
</Warning>

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

Сначала важно правильно понять ментальную модель: **backend log — это журнал тарификации, а не журнал ошибок.**

| Что вы ищете                                        | Backend [log page](https://api.apiyi.com/log)              | API response                                     |
| --------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------ |
| Сумма, списанная за этот вызов                      | Да                                                         | Нет (`usage` показывает только количество token) |
| Использование token                                 | Да                                                         | Да (поле `usage`)                                |
| Имя модели, время вызова                            | Да (только успешные вызовы)                                | Вы должны записывать это сами                    |
| `request_id`                                        | Да (только успешные вызовы)                                | Заголовок ответа `x-request-id`                  |
| **Код ошибки и необработанное сообщение об ошибке** | **Нет**                                                    | **Единственный источник**                        |
| **Конкретная причина отказа на стороне upstream**   | **Нет**                                                    | **Единственный источник**                        |
| Сам неудачный вызов                                 | **Не перечисляется** (нет списания — нет записи в журнале) | —                                                |

<Tip>
  **Если читать это в обратном порядке, это становится самым сильным одиночным тестом на проблемы с соединением**: если журнал **содержит** запись о тарификации, запрос дошел до upstream и потребил ресурсы; если **не содержит**, проблема почти наверняка возникла до достижения upstream (сеть, аутентификация, проверка параметров). Полные подробности в [Чтении списанных сумм в журнале](/ru/faq/log-billing-explained).
</Tip>

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

Это все, что нужно, чтобы диагностировать один неудачный вызов. Если не хватает хотя бы одного из них, диагностика снова превращается в догадки:

| Поле                                                        | Как получить                                                                    | Что ломается без него                                                                                                                         |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| **Код состояния HTTP**                                      | `resp.status_code` / `err.status`                                               | Вы не сможете отличить отклоненный запрос (4xx) от сбоя сервера (5xx) и от соединения, которое так и не было установлено (вообще без статуса) |
| **Полное тело ответа**                                      | `resp.text` / `await resp.text()`                                               | **Самое важное** — настоящая причина находится здесь. Потеряйте его, и все, что останется, — это догадки                                      |
| **`x-request-id` заголовок ответа**                         | `resp.headers.get("x-request-id")`                                              | Служба поддержки не сможет точно привязать этот вызов и вместо этого будет вынуждена искать в неточном временном окне                         |
| **Время вызова с часовым поясом**                           | Записывайте на стороне клиента, например `2026-08-03 15:44 (UTC+8)`             | Наши клиенты находятся по всему миру; временную метку без часового пояса нельзя сопоставить с логами                                          |
| **Имя модели + путь эндпоинта**                             | Из вашего собственного запроса                                                  | Одна и та же модель ведет себя по-разному на разных эндпоинтах (`/v1/images/edits` vs `/v1/chat/completions`)                                 |
| **Ключевые параметры запроса**                              | `size`, `quality`, количество reference-image и их размер, `max_tokens` и т. д. | Проблемы с параметрами нельзя воспроизвести; при проблемах с изображениями нельзя понять, виноват ли сам актив                                |
| **Необработанное исключение клиента + количество повторов** | `repr(e)` и одна строка лога на каждую попытку                                  | После успешного повтора в логах остается один чистый 200, и вы никогда не увидите, сколько раз транспорт на самом деле сбоил                  |

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

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

По сути, есть только один принцип: **перехватывайте на двух уровнях и ничего не теряйте ни на одном из них.**

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

### Python / requests

```python theme={null}
import time
import requests

BASE_URL = "https://api.apiyi.com/v1"
API_KEY = "sk-your-api-key"          # read from an environment variable in production


def call_and_log(path, payload, timeout=300):
    started = time.strftime("%Y-%m-%d %H:%M:%S %z")      # includes timezone
    try:
        resp = requests.post(
            f"{BASE_URL}{path}",
            headers={"Authorization": f"Bearer {API_KEY}"},
            json=payload,
            timeout=timeout,
        )
    except requests.exceptions.Timeout as exc:
        # Transport layer: timed out, no HTTP response to read
        raise RuntimeError(f"[{started}] timed out after {timeout}s: {exc!r}") from exc
    except requests.exceptions.RequestException as exc:
        # Transport layer: connection reset, SSL error, DNS failure — still no body
        raise RuntimeError(f"[{started}] transport failure: {exc!r}") from exc

    if resp.status_code >= 400:
        # The point: pass the body through verbatim, don't reword it here
        raise RuntimeError(
            f"[{started}] HTTP {resp.status_code} {path} "
            f"model={payload.get('model')}\n"
            f"x-request-id: {resp.headers.get('x-request-id')}\n"
            f"{resp.text}"
        )
    return resp.json()
```

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

### Python / OpenAI SDK

Официальный SDK уже прикрепляет к объекту исключения все три части. Большинство людей вместо этого просто `print` собственное однострочное сообщение:

```python theme={null}
from openai import OpenAI, APIStatusError, APIConnectionError

client = OpenAI(
    api_key="sk-your-api-key",
    base_url="https://api.apiyi.com/v1",
    max_retries=3,      # built-in exponential backoff for 429 / 5xx / connection errors
    timeout=60.0,
)

try:
    resp = client.chat.completions.create(
        model="gpt-5.4",
        messages=[{"role": "user", "content": "Hello"}],
    )
except APIStatusError as e:
    # Server returned 4xx/5xx: status, request-id and body are all right here
    print("status code :", e.status_code)
    print("request-id  :", e.request_id)
    print("raw body    :", e.response.text)
    raise
except APIConnectionError as e:
    # No HTTP response: connection reset, timeout, local proxy failure
    print("transport   :", repr(e), "|", repr(e.__cause__))
    raise
```

<Tip>
  Даже однострочное сообщение должно быть `print(f"API error: {e}")`, а не `print("request failed")` — `str(e)` исключения SDK **уже содержит сообщение сервера**. На самом деле информация теряется, когда вы полностью выбрасываете объект исключения.
</Tip>

### Node.js

С SDK:

```javascript theme={null}
import OpenAI from 'openai';

const client = new OpenAI({
  apiKey: 'sk-your-api-key',
  baseURL: 'https://api.apiyi.com/v1',
});

try {
  const resp = await client.images.edit({ /* ... */ });
} catch (err) {
  if (err instanceof OpenAI.APIError) {
    // Server returned 4xx/5xx
    console.error('status code :', err.status);
    console.error('request-id  :', err.requestID);
    console.error('raw body    :', JSON.stringify(err.error));
  } else {
    // Transport layer: ECONNRESET, UND_ERR_*, etc. — no HTTP response
    console.error('transport   :', err.code, err.message, err.cause);
  }
  throw err;
}
```

С обычным `fetch` **чаще всего ошибка возникает именно здесь**:

```javascript theme={null}
const resp = await fetch('https://api.apiyi.com/v1/images/edits', {
  method: 'POST',
  headers: { Authorization: 'Bearer sk-your-api-key' },
  body: form,
});

if (!resp.ok) {
  const raw = await resp.text();        // read the body first, then throw
  throw new Error(
    `HTTP ${resp.status} ${resp.url}\n` +
    `x-request-id: ${resp.headers.get('x-request-id')}\n${raw}`
  );
}
```

<Warning>
  Тот самый `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()`, прежде чем выбрасывать исключение** — одна эта строка отделяет диагностируемый отчет от неразрешимого.
</Warning>

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

Когда вы просите кого-то воспроизвести проблему, эта команда требует меньше всего усилий — она одним запуском показывает код состояния, заголовки, тело и время выполнения:

```bash theme={null}
curl -i -sS -X POST https://api.apiyi.com/v1/images/edits \
  -H "Authorization: Bearer sk-your-api-key" \
  -F "model=gpt-image-2" \
  -F "image=@input.png" \
  -F "prompt=replace the background with plain white" \
  -w '\n---\nHTTP %{http_code}  total %{time_total}s\n'
```

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

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

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

Эта ошибка пришла из узла ComfyUI клиента:

```text theme={null}
Upstream HTTP 0: OpenSSL SSL_read: Connection was reset, errno 10054
```

Это куда уродливее, чем `400 Bad Request` — и при этом оно **полное**, поэтому направление определяется за секунды:

| Фрагмент                         | Значение                                                                                                                                                        |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `HTTP 0`                         | Вообще не было получено никакого HTTP-ответа. `0` — это псевдокод статуса, означающий, что не пришла даже строка статуса — это не бизнес-ошибка в стиле 400/500 |
| `SSL_read: Connection was reset` | Соединение было разорвано удаленной стороной или промежуточным узлом на этапе чтения TLS                                                                        |
| `errno 10054`                    | Windows `WSAECONNRESET`, эквивалент `ECONNRESET` в Linux — был получен TCP RST                                                                                  |

Вывод следует немедленно: это проблема **транспортного уровня**, не связанная с безопасностью контента или параметрами, и она не приводит к списанию (запрос так и не был завершен). Путь диагностики: [Обрывы соединения с Image API](/ru/api-capabilities/image-connection-drops).

<Info>
  **Сравните два варианта**: один аккуратно упакован и ничего не объясняет (`400 Bad Request`); другой длинный и уродливый и прямо указывает на коренную причину (`errno 10054`). **Для диагностики сырая, уродливая и полная ошибка всегда лучше дружелюбной, аккуратной, переписанной версии.**
</Info>

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

| Инструмент             | Где находится сырая ошибка                                                                                                                                          |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ComfyUI                | Красный текст на узле обычно обрезан; полный stack trace находится в **окне терминала, из которого вы запустили ComfyUI**, или в `comfyui.log` в каталоге установки |
| Dify / Coze / n8n      | Откройте запись запуска, разверните детали выполнения этого узла и прочитайте сырой HTTP-ответ, а не сводку ошибки узла                                             |
| LangChain / LlamaIndex | Поймайте `openai.APIStatusError` вместо голого `Exception` — см. шаблон SDK выше                                                                                    |
| Desktop clients        | Включите журнал отладки / разработчика в настройках или воспроизведите один раз с `curl`                                                                            |

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

<Steps>
  <Step title="Не переписывайте, а только пропускайте дальше">
    Промежуточный слой может **добавлять** контекст (какой сервис, какой tenant, какая попытка повтора), но не должен **заменять** вышестоящий `error.message`. После переписывания больше негде восстановить исходный текст.
  </Step>

  <Step title="Храните сообщение для пользователя отдельно от исходного">
    Следуйте трехчастной структуре, использованной в [обработке ошибок генерации изображений Gemini](/ru/api-capabilities/gemini-image-error-handling): `userMessage` (дружелюбный текст для конечных пользователей), `devMessage` (ваша классификация для разработчиков) и `rawResponse` (тело ответа без изменений). Первые два поля можно свободно полировать; третье храните дословно.
  </Step>

  <Step title="Никогда не выдавайте неизвестную ошибку">
    В резервной ветке запишите `status`, `x-request-id` и первые 2000 символов тела. «Неклассифицированная ошибка», которая сохраняет исходный текст, поддается диагностике; чистая «неизвестная ошибка» — нет.
  </Step>
</Steps>

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

* `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, и она **воспроизводится стабильно** (за это не тарифицируются; см. [Обрывы соединения](/ru/api-capabilities/image-connection-drops));
* Вы подозреваете, что тарификация не совпадает с вашими фактическими вызовами — только `request_id` позволяет точно сверить данные.

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

```text theme={null}
[Summary]      gpt-image-2 image edit endpoint consistently returns 400
[Endpoint]     POST https://api.apiyi.com/v1/images/edits
[Model]        gpt-image-2
[Call time]    2026-08-03 15:44 (UTC+8)
[request-id]   copy from the x-request-id response header
[HTTP status]  400
[Raw body]
{"error":{"message":"...","type":"...","code":"..."}}
[Key params]   size=2048x2048, quality=high, 1 reference image / 3.2 MB / PNG
[Reproduction] 5 consecutive calls, 5 failures; works again with a different reference image
[Already checked] key valid, balance sufficient, same key works on text models
```

<Card title="Поддержка WeCom" icon="headphones" href="https://work.weixin.qq.com/kfid/kfc9adfd5810ece25ec">
  <img src="https://mintcdn.com/apiyillc/fpi567ydpk7adDt0/images/wecom-qrcode.png?fit=max&auto=format&n=fpi567ydpk7adDt0&q=85&s=7286b96e94110e3a48798b649df1b45b" alt="QR-код поддержки WeCom" style={{maxWidth: "180px"}} width="400" height="400" data-path="images/wecom-qrcode.png" />

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

  Также вы можете связаться с нами в Telegram по `@apiyi001` или по email `hi@apiyi.com`.
</Card>

<Tip>
  Отправьте шаблон выше **в виде текста** — это намного эффективнее любого описания. С `request_id` мы сможем сразу перейти к полной трассировке этого единственного вызова, вместо того чтобы спрашивать: «примерно когда и какая модель?» См. [API запроса логов](/ru/api-capabilities/log-query), чтобы узнать, как найти `request_id`.
</Tip>

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

<CardGroup cols={3}>
  <Card title="Руководство по API" icon="book" href="/ru/api-manual">
    Распространенные коды ошибок, аутентификация и лимиты запросов
  </Card>

  <Card title="Обрывы соединения" icon="unplug" href="/ru/api-capabilities/image-connection-drops">
    Полный путь устранения неполадок для `ECONNRESET`, `errno 10054` и SSL EOF
  </Card>

  <Card title="API запроса логов" icon="file-text" href="/ru/api-capabilities/log-query">
    Получайте журналы вызовов через API — как искать `request_id` и сверять тарификацию
  </Card>

  <Card title="Чтение начисленных сумм" icon="receipt" href="/ru/faq/log-billing-explained">
    Почему неудачные вызовы никогда не попадают в лог и как использовать это как диагностический тест
  </Card>

  <Card title="Настройка таймаута" icon="hourglass" href="/ru/faq/timeout-configuration">
    Уровни таймаута по типам моделей и что проверять, если его увеличение не помогает
  </Card>

  <Card title="Основы Image API" icon="book-check" href="/ru/api-capabilities/image-api-best-practices">
    Синхронные вызовы, различия префиксов base64, предварительная обработка `400 invalid_image_file`
  </Card>
</CardGroup>
