Diil Docs
  1. Документація
  2. Посібники

Помилки: кожен код і що з ним робити

Оновлено:

Усі помилки, які може повернути Content API, в одному місці: як виглядає тіло відповіді, чому вона зазвичай виникає і як її виправити. API лише читає дані й доволі компактний, тож список короткий — а кожна помилка має машиночитний message, щоб вашому коду не доводилося нічого вгадувати.

Усі помилки одним поглядом

СтатусmessageОдним реченням
400invalid_markerМаркер в URL — некоректний.
400invalid_langКод мови в lang має неправильний формат.
400unknown_langКод мови правильний, але активної мови з таким кодом на сайті немає.
400too_many_langsПонад 50 кодів у lang.
401invalid_keyКлюча сайту немає, він некоректний або відкликаний.
404page_not_foundСторінки з таким маркером немає.
404section_not_foundСекції з таким маркером немає.
404block_not_foundБлоку з таким маркером немає (у цій секції).
404post_not_foundНемає опублікованого допису блогу з таким slug.
404Cannot GET /v1/…Такої адреси в API взагалі не існує.
405method_not_allowedБудь-який метод, окрім GET чи HEAD.
429rate_limit_exceededЗабагато запитів за цю хвилину.
5xx—Щось зламалося на нашому боці.

Як виглядає помилка

Тіло будь-якої помилки — це JSON із полем message. Деякі помилки додають кілька корисних полів — наприклад, unknown_lang підкаже, які мови на сайті є насправді.

Типова відповідь з помилкоюhttp
HTTP/1.1 404 Not Found
Content-Type: application/json
Cache-Control: no-store

{"message":"page_not_found"}

Помилки завжди надходять із Cache-Control: no-store, тож 404 для сторінки, яку редактор створює просто зараз, не застрягне в кеші браузера чи CDN. Усуньте причину — і наступний запит отримає нормальну відповідь.

400 Bad Request

Помилка в самому запиті. Повторна спроба не допоможе — виправте URL.

invalid_marker

400json
{"message":"invalid_marker"}

Маркери сторінок, секцій і блоків мають відповідати ^[A-Za-z0-9_]{2,40}$: латинські літери, цифри та підкреслення, від 2 до 40 символів.

Типові причини:

  • Дефіс або крапка: /v1/pages/about-us — у маркерах підкреслення, about_us.
  • Шлях замість маркера: href сторінки (/about) — це не її маркер (about).
  • Один символ, понад 40 символів, пробіли чи нелатинські літери.
  • Сегмент URL від відвідувача передано в API як є — хтось набрав в адресному рядку /über-uns.

Як виправити: скопіюйте маркер із CRM. Якщо у ваших маршрутах дефіси, перетворюйте їх на маркери в роутері. А коли маркер приходить з URL відвідувача, обробляйте invalid_marker так само, як 404: такої сторінки просто немає.

invalid_lang

400json
{"message":"invalid_lang","lang":"EN"}

Кожен код у lang має виглядати як ^[a-z]{2}(-[A-Za-z]{2,4})?$: дві малі літери, за якими може йти регіон, як-от pt-BR. Перевірка чутлива до регістру. У полі lang повертається значення, яке не пройшло перевірку.

Типові причини:

  • Великі літери: EN замість en.
  • Локаль із підкресленням від сервера чи i18n-бібліотеки: en_US.
  • Трилітерні коди на кшталт eng або повна назва, як-от english.

Як виправити: використовуйте саме ті коди, які є на сайті, — їх повертає GET /v1/langs. Найнадійніше — зводити те, що дає ваш фреймворк, до одного з цих кодів.

unknown_lang

400json
{
  "message": "unknown_lang",
  "lang": "fr",
  "unknown": ["fr"],
  "available": ["en", "de"]
}

Код має правильний формат, але це не активна мова цього сайту. Тіло відповіді справді корисне: unknown перелічує всі запитані коди, що не пройшли, available — ті, якими можна користуватися.

Типові причини:

  • Редактор вимкнув мову в CRM, а ваш код досі запитує її за назвою.
  • Локаль браузера передано як є: en-US — коректний формат, але на сайті є лише en.
  • Код сусідньої мови чи країни: ua там, де на сайті uk, cz там, де cs.
  • Ключ належить іншому сайту з іншим набором мов.

Як виправити: будуйте список мов із GET /v1/langs, а не прописуйте його в коді, або переходьте на перший код з available. Якщо вам однаково потрібні всі мови, просто не передавайте lang. Докладніше — Мови та запасна мова.

too_many_langs

400json
{"message":"too_many_langs","max":50}

За один запит можна попросити щонайбільше 50 кодів. Якщо ви вперлися в це обмеження, найімовірніше, список будується з чогось необмеженого — заголовка, введення користувача, циклу, який усе додає й додає.

Як виправити: щоб отримати всі активні мови, просто не передавайте lang — саме це він і робить за замовчуванням.

401 invalid_key

401json
{"message":"invalid_key"}

API не прийняв ключ сайту. Коректний ключ — це pk_ і 32 малі шістнадцяткові символи; його передають у заголовку x-crm-key (або як ?key=).

Типові причини:

  • Заголовка немає — найчастіше тому, що змінна оточення в цьому середовищі порожня.
  • У браузері змінна так і не потрапила в бандл (у Next.js туди потрапляють лише змінні NEXT_PUBLIC_*), тож у заголовку буквально написано undefined.
  • Разом із ключем у .env скопіювалися лапки, пробіли чи перенесення рядка в кінці.
  • Ключ відкликали в CRM.
  • Ключ створили щойно. Зазвичай він працює одразу, але в гіршому разі розпізнавання може зайняти кілька хвилин. Те саме стосується відкликання.

Як виправити: перевірте ключ у CRM у розділі Налаштування → Ключі Content API і протестуйте його через curl. Подробиці — на сторінці Ключ сайту.

Чи живий цей ключ?bash
curl -i "https://back.sitecog.com/content/v1/langs" -H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"

404 Not Found: такого вмісту немає

URL правильний, ключ правильний, але за цією адресою нічого немає. message підкаже, чого саме бракує:

messageEndpointЗвичні підозрювані
page_not_found/v1/pages/:markerДрукарська помилка чи неправильний регістр (Home ≠ home), сторінку перейменували чи видалили в CRM, ключ іншого сайту.
section_not_found/v1/sections/:markerТе саме. Зверніть увагу: приховані секції (show: false) однаково повертаються — тож 404 справді означає, що секції немає.
block_not_found/v1/blocks/:markerДрукарська помилка, або блок живе в іншій секції, ніж та, яку ви передали в ?section=.
post_not_found/v1/blog/:slugДопис — чернетка, запланований на майбутнє, або slug некоректний (лише малі a–z, цифри й дефіси, до 120 символів — некоректний slug дає 404, а не 400).
404json
{"message":"page_not_found"}

Як виправити: якщо маркер прописано у ваших шаблонах, звірте його з CRM. Якщо він приходить з URL відвідувача, це взагалі не баг — це звичайна 404, тож покажіть свою сторінку «не знайдено» (у Next.js — викличте notFound()).

Невідомий маршрут

Якщо URL не відповідає жодному endpoint, ви отримаєте стандартну 404 фреймворку, яка виглядає інакше, ніж 404 для вмісту вище:

404json
{
  "message": "Cannot GET /v1/page/home",
  "error": "Not Found",
  "statusCode": 404
}

Її легко впізнати за полями error і statusCode та за повідомленням Cannot GET …. Це завжди баг у вашому коді, а не відсутній вміст.

Типові причини:

  • Однина замість множини: /v1/page/home, /v1/block/phone.
  • Забутий /v1 у шляху.
  • Зайві сегменти: /v1/pages/home/hero — у секцій свій endpoint, /v1/sections/hero.

Як виправити: порівняйте URL із довідником API. Базовий URL в одній константі (https://back.sitecog.com/content) і побудова шляхів в одному хелпері рятують від більшості таких помилок.

405 method_not_allowed

405json
{"message":"method_not_allowed"}

Content API лише читає: він відповідає на GET і HEAD. POST, PUT, PATCH і DELETE отримують 405.

Типові причини: HTTP-клієнт, що за замовчуванням шле POST, форма, чий action вказує на API, або надія зберігати через нього вміст. Вміст редагують у CRM чи просто на сайті в режимі Live — API лише читає.

429 rate_limit_exceeded

429json
{"message":"rate_limit_exceeded"}

Забагато запитів за поточну хвилину — з вашої IP (300), з вашим ключем (600) або забагато спроб із неправильним ключем з вашої IP (20). Заголовка Retry-After немає: зачекайте до наступної хвилини з невеликим випадковим зсувом. Ліміти, розрахунки й готовий хелпер для повторів — на сторінці Ліміти запитів.

5xx: наш бік

500, 502, 503 чи 504 означають, що щось пішло не так у нас, а не у вашому коді. Саме такі помилки варто повторювати: коротка експоненційна затримка з випадковим зсувом (приклад), а якщо й далі не виходить — покажіть останню вдалу копію з кешу. Сайт, що під час збою показує вчорашній текст, значно кращий за сайт зі стек-трейсом.

Мережеві помилки та CORS

Буває, що відповіді немає взагалі — fetch відхиляється з TypeError («Failed to fetch», «fetch failed»). Причини:

  • Проблеми з DNS, немає з’єднання, файрвол на вашому сервері, запит, що завис. Задайте тайм-аут: AbortSignal.timeout(10_000).
  • CORS у браузері. API дозволяє будь-яке джерело, методи GET та OPTIONS і заголовки запиту x-crm-key, content-type та if-none-match. Додасте будь-який інший власний заголовок (Authorization, заголовок трасування від вашого HTTP-клієнта) — і браузер заблокує запит ще до відправлення, а ваш код побачить мережеву помилку. Загляньте в консоль: браузер скаже, який заголовок йому не сподобався.

Надійний обробник помилок на TypeScript

Зберіть усе описане в один невеликий модуль: кожна відповідь не з 2xx і кожен мережевий збій перетворюються на типізовану ContentApiError зі статусом, машиночитним кодом і додатковими полями. Решта коду просто перевіряє err.code.

lib/content-api.tsts
const API = 'https://back.sitecog.com/content';

export type ContentErrorCode =
  | 'invalid_marker' | 'invalid_lang' | 'unknown_lang' | 'too_many_langs'
  | 'invalid_key'
  | 'page_not_found' | 'section_not_found' | 'block_not_found' | 'post_not_found'
  | 'method_not_allowed' | 'rate_limit_exceeded'
  | 'unknown_route'   // 404 "Cannot GET …" — неправильний URL
  | 'server_error'    // 5xx
  | 'network_error'   // відповіді немає взагалі
  | 'unexpected';     // усе, чого ми ще не бачили

const KNOWN = new Set<string>([
  'invalid_marker', 'invalid_lang', 'unknown_lang', 'too_many_langs', 'invalid_key',
  'page_not_found', 'section_not_found', 'block_not_found', 'post_not_found',
  'method_not_allowed', 'rate_limit_exceeded',
]);

type ErrorBody = {
  message?: string;
  lang?: string;
  unknown?: string[];
  available?: string[];
  max?: number;
};

export class ContentApiError extends Error {
  readonly status: number;          // HTTP-статус, 0 — якщо відповіді не було
  readonly code: ContentErrorCode;
  readonly lang?: string;           // invalid_lang, unknown_lang
  readonly unknown?: string[];      // unknown_lang: коди, що не пройшли
  readonly available?: string[];    // unknown_lang: коди, якими можна користуватися
  readonly max?: number;            // too_many_langs

  constructor(status: number, code: ContentErrorCode, body: ErrorBody = {}, cause?: unknown) {
    super(`Content API ${status || 'network'}: ${body.message ?? code}`, { cause });
    this.name = 'ContentApiError';
    this.status = status;
    this.code = code;
    this.lang = body.lang;
    this.unknown = body.unknown;
    this.available = body.available;
    this.max = body.max;
  }

  /** Варто спробувати пізніше: проблеми з мережею, ліміт запитів, наші 5xx */
  get retryable() {
    return this.status === 0 || this.status === 429 || this.status >= 500;
  }

  /** Вмісту не існує — покажіть свою сторінку 404 */
  get notFound() {
    return this.code.endsWith('_not_found') || this.code === 'invalid_marker';
  }
}

function toCode(status: number, message?: string): ContentErrorCode {
  if (status >= 500) return 'server_error';
  if (message && KNOWN.has(message)) return message as ContentErrorCode;
  if (status === 404 && message?.startsWith('Cannot ')) return 'unknown_route';
  return 'unexpected';
}

export async function contentFetch<T>(path: string, init: RequestInit = {}): Promise<T> {
  let res: Response;
  try {
    res = await fetch(API + path, {
      ...init,
      headers: { ...(init.headers as Record<string, string>), 'x-crm-key': process.env.CRM_KEY! },
      signal: init.signal ?? AbortSignal.timeout(10_000),
    });
  } catch (cause) {
    throw new ContentApiError(0, 'network_error', {}, cause);
  }

  if (res.ok) return (await res.json()) as T;

  // Тіла помилок — JSON, але проксі дорогою може відповісти HTML — обережно
  const body: ErrorBody = await res.json().catch(() => ({}));
  throw new ContentApiError(res.status, toCode(res.status, body.message), body);
}

А ось як це виглядає в реальному коді:

// app/[lang]/[marker]/page.tsx
import { notFound, redirect } from 'next/navigation';
import { contentFetch, ContentApiError } from '@/lib/content-api';

type Props = { params: Promise<{ lang: string; marker: string }> };

export default async function Page({ params }: Props) {
  const { lang, marker } = await params;

  try {
    const page = await contentFetch<PageData>(`/v1/pages/${marker}?lang=${lang}`, {
      next: { revalidate: 60 },
    });
    return <PageView page={page} lang={lang} />;
  } catch (err) {
    if (err instanceof ContentApiError) {
      if (err.notFound) notFound();
      if (err.code === 'unknown_lang' && err.available?.length) {
        redirect(`/${err.available[0]}/${marker}`);
      }
      console.error(err.code, err.status, err.message); // invalid_key, unknown_route… — баг, який треба виправити
    }
    throw err; // error.tsx покаже «щось пішло не так»
  }
}