0% прочитано

Безпечні помилки API: що показувати клієнту, а що залишати для діагностики

Помилка API має бути корисною для клієнта, але не повинна розкривати внутрішню будову системи. Показую, як я розділяю стабільну зовнішню відповідь, технічну причину помилки та correlation ID для діагностики.

1 жовтня 2026 р. 8 хв читанняЛаравель

Коли API повертає помилку, є дві різні потреби.

Клієнту потрібно зрозуміти, що сталося і як система класифікує цю помилку. Розробнику всередині системи часто потрібно значно більше: точна причина, місце виникнення, виняток і технічний контекст.

Проблема починається, коли ці два рівні змішуються.

Під час роботи над зовнішнім API одного з моїх проєктів я окремо зафіксувала для себе правило: внутрішній виняток не повинен автоматично ставати публічною відповіддю API.

Зовнішній контракт має бути стабільним і безпечним. Внутрішня діагностика може бути значно точнішою.

Публічна помилка є частиною контракту API. Текст внутрішнього винятку є деталлю реалізації. Це різні речі.

Текст винятку не є відповіддю API

PHP - Внутрішній виняток напряму потрапляє назовні
try {    $service->execute($request);} catch (Throwable $exception) {    return response()->json([        'error' => $exception->getMessage(),    ], 500);}

Такий код зручний під час швидкої розробки, але для зовнішнього API він створює одразу кілька проблем.

Повідомлення винятку може містити:

  • назви внутрішніх класів або полів;
  • деталі структури бази даних;
  • внутрішній стан;
  • технічну причину, яку клієнту не потрібно знати;
  • дані, які взагалі не повинні залишати межі системи.

Крім того, текст винятку легко змінюється під час рефакторингу. Якщо клієнт починає залежати від цього тексту, внутрішня деталь непомітно стає частиною зовнішнього контракту.

Назовні я віддаю стабільний опис проблеми

JSON - Узагальнений приклад публічної помилки
{  "type": "https://api.example.com/problems/request-conflict",  "title": "Request conflict",  "status": 409,  "detail": "The request cannot be completed in the current state.",  "code": "request_conflict",  "correlationId": "7efc7f19-..."}

Публічна відповідь повинна містити лише те, на що клієнт справді може покладатися.

Для мене тут важливі кілька окремих частин:

  • HTTP status описує результат на рівні HTTP;
  • стабільний код дозволяє програмно розрізняти класи помилок;
  • безпечний текст пояснює проблему людині;
  • correlation ID дозволяє знайти відповідний запит у внутрішній діагностиці.

У прикладі я використовую структуру, близьку до Problem Details. Конкретні коди й адреси тут навмисно умовні.

Код помилки стабільніший за текст

Людині зручніше читати повідомлення. Програмі надійніше працювати зі стабільним кодом.

Наприклад, текст можна змінити з:

The request cannot be completed.

на більш зрозумілий:

The request cannot be completed in the current state.

Для клієнта це не повинно означати зміну логіки.

Якщо програмний клієнт перевіряє стабільний code, текст можна покращувати без ламання інтеграції.

HTTP status і код помилки вирішують різні задачі

Один HTTP status може мати кілька причин
HTTP 409    |    +--> request_conflict    +--> state_conflict    +--> duplicate_operation HTTP 422    |    +--> validation_failed

Я не використовую HTTP status як власну систему внутрішніх номерів помилок.

409, 422 або 503 мають стандартний зміст на рівні протоколу.

А вже стабільний код помилки уточнює, до якого класу належить конкретна проблема.

Так зовнішній контракт залишається зрозумілим і для звичайних HTTP-клієнтів, і для програмного коду, якому потрібна точніша класифікація.

Іноді менш точна відповідь є безпечнішою

Є помилки, де максимальна точність зовнішньої відповіді не допомагає клієнту, зате може допомогти тому, хто досліджує внутрішній стан системи.

Наприклад, для чутливої перевірки всередині можуть існувати різні причини відмови. Але назовні вони можуть навмисно виглядати однаково.

Це не приховування помилки від клієнта. Це правильно визначена межа інформації.

Клієнт отримує достатню відповідь для своєї наступної дії. Внутрішня система зберігає точнішу причину для розслідування.

Чим чутливіша операція, тим уважніше я перевіряю, чи кожна додаткова деталь у відповіді справді потрібна зовнішньому клієнту.

Correlation ID з’єднує дві сторони помилки

Зв’язок публічної помилки з діагностикою
Клієнт    |    | отримує безпечну помилку    | + correlation ID    vAPI response correlation ID    |    v внутрішня діагностика    |    +--> технічна причина    +--> виняток    +--> час запиту

Якщо клієнту не потрібно бачити внутрішній виняток, виникає інше питання: як потім знайти конкретну помилку?

Для цього я використовую ідентифікатор запиту - correlation ID.

Клієнт може передати його під час звернення в підтримку або додати до власних журналів. На внутрішній стороні за тим самим значенням можна знайти технічний контекст конкретного запиту.

Таким чином мені не потрібно вибирати між двома поганими варіантами: або показувати внутрішні деталі клієнту, або втрачати можливість діагностики.

Що залишати для внутрішньої діагностики

Внутрішня діагностика може бути значно точнішою за публічну відповідь.

Залежно від системи туди можуть входити:

  • correlation ID;
  • тип внутрішньої помилки або винятку;
  • технічна причина;
  • час виникнення;
  • операція, під час якої сталася помилка;
  • обмежений контекст, необхідний для розслідування.

Це може бути звичайний журнал, централізована система логування, error tracker або інший механізм спостереження.

Але внутрішній журнал теж не повинен перетворюватися на безконтрольний дамп усіх даних запиту.

Внутрішній лог теж не є місцем для секретів

PHP - Занадто багато даних у журналі
Log::error('API request failed', [    'exception' => $exception,    'request' => $request->all(),    'authorization' => $request->header('Authorization'),]);
PHP - Обмежений діагностичний контекст
Log::error('API request failed', [    'correlation_id' => $correlationId,    'exception' => $exception::class,    'operation' => 'example_operation',]);

Те, що інформація не повертається клієнту, ще не означає, що її потрібно автоматично записувати в лог.

Сирі токени, паролі, секрети, повні заголовки авторизації та інші чутливі значення можуть створити вже інший витік - через систему журналювання.

Тому я намагаюся логувати не "весь запит", а лише той контекст, який реально потрібен для пошуку причини.

Межа API перетворює внутрішню помилку на зовнішній контракт

PHP - Внутрішня причина не стає публічним текстом автоматично
try {    $service->execute($input);} catch (WriteConflict $exception) {    report($exception);     return ProblemResponse::make(        status: 409,        code: 'request_conflict',        title: 'Request conflict',        detail: 'The request cannot be completed in the current state.',        correlationId: $correlationId,    );}

Мені подобається, коли це перетворення видно в коді явно.

Внутрішня частина системи повідомляє про конкретну проблему своєю мовою. Межа API вирішує:

  • який HTTP status відповідає ситуації;
  • який стабільний код дозволено показати клієнту;
  • який текст є достатнім і безпечним;
  • який correlation ID додати до відповіді.

Так внутрішня модель може змінюватися, не змушуючи зовнішніх клієнтів знати про кожен новий клас винятку.

Що я перевіряю для кожної нової помилки API

Перед тим як додати нову помилку до зовнішнього API, я перевіряю:

  • Що реально потрібно знати клієнту?
  • Чи не розкриває текст внутрішній стан або технічну будову?
  • Чи має помилка стабільний програмний код?
  • Чи відповідає HTTP status своїй стандартній семантиці?
  • Чи можна знайти конкретний випадок за correlation ID?
  • Чи не потрапляють секрети ні у відповідь, ні у внутрішні журнали?
  • Чи може внутрішній код змінитися без зміни зовнішнього контракту?

Помилка API - це теж контракт

Раніше на помилки легко дивитися як на другорядну частину API: головне повернути правильні дані при успіху.

Але для інтеграції помилка є такою самою частиною контракту, як успішна відповідь.

Клієнту потрібні стабільний код, правильний HTTP status і достатнє пояснення. Розробнику потрібна точна причина та контекст для розслідування.

correlation ID дозволяє з’єднати ці дві сторони без необхідності показувати внутрішню діагностику назовні.

Для мене безпечна помилка API - це не помилка з мінімумом інформації. Це помилка, де кожна деталь знаходиться на правильній стороні межі.

Клієнту потрібна стабільна відповідь. Розробнику потрібна точна причина. Хороший API не змушує вибирати щось одне.