Diil Docs
  1. Документация
  2. Руководства

Ошибки: каждый код и что с ним делать

Обновлено:

Все ошибки, которые может вернуть Content API, в одном месте: как выглядит ответ, отчего это обычно случается и как починить. API только читает и сам по себе небольшой, поэтому список короткий, а у каждой ошибки есть машиночитаемое поле message — вашему коду не придётся гадать.

Все ошибки одной таблицей

СтатусmessageЕсли в двух словах
400invalid_markerМаркер в адресе — не маркер.
400invalid_langКод языка в lang записан неправильно.
400unknown_langКод записан верно, но такого активного языка у сайта нет.
400too_many_langsВ lang больше 50 кодов.
401invalid_keyКлюча сайта нет, он неправильного вида или отозван.
404page_not_foundНет страницы с таким маркером.
404section_not_foundНет секции с таким маркером.
404block_not_foundНет блока с таким маркером (в этой секции).
404post_not_foundНет видимой статьи блога с таким адресом.
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

Неправилен сам запрос. Повтор не поможет — нужно исправить адрес.

invalid_marker

400json
{"message":"invalid_marker"}

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

Частые причины:

  • Дефис или точка: /v1/pages/about-us — в маркерах подчёркивание, about_us.
  • Путь вместо маркера: href страницы (/about) — это не её маркер (about).
  • Один символ, больше 40 символов, пробелы или кириллица.
  • Кусок адреса от посетителя ушёл в API как есть — кто-то набрал в строке браузера /о-нас.

Что делать: скопируйте маркер из CRM. Если в ваших адресах дефисы, сопоставляйте их с маркерами в роутере. А если маркер пришёл из адреса посетителя, считайте 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 — по умолчанию API именно так и делает.

401 invalid_key

401json
{"message":"invalid_key"}

API не принял ключ сайта. Правильный ключ — это pk_ и 32 строчных шестнадцатеричных символа, передаётся в заголовке x-crm-key (или параметром ?key=).

Частые причины:

  • Заголовка нет — чаще всего потому, что переменная окружения пуста именно в этом окружении.
  • В браузере переменная не попала в сборку (в Next.js туда попадают только NEXT_PUBLIC_*), и в заголовке буквально написано undefined.
  • Кавычки, пробелы или перевод строки, скопированные в .env вместе с ключом.
  • Ключ отозвали в CRM.
  • Ключ создан только что. Обычно он начинает работать сразу, в худшем случае — через несколько минут. С отзывом ключа то же самое.

Что делать: проверьте ключ в CRM в разделе «Настройки → Ключи контентного API» и прогоните его через curl. Подробности — на странице Ключ сайта.

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

404 Not Found: такого контента нет

Адрес правильный, ключ правильный, но по этому адресу ничего нет. Что именно не нашлось, подскажет message:

messageЗапросОбычные подозреваемые
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Статья — черновик, запланирована на будущее, или адрес статьи неправильный (только строчные a–z, цифры и дефисы, до 120 символов — на неправильный адрес приходит 404, а не 400).
404json
{"message":"page_not_found"}

Что делать: если маркер зашит в шаблон, сверьте его с CRM. Если он пришёл из адреса посетителя, это вовсе не ошибка в коде, а обычный 404 — покажите свою страницу «не найдено» (в Next.js — вызовом notFound()).

Неизвестный адрес

Если адрес не совпадает ни с одним запросом API, приходит стандартный 404 фреймворка — и выглядит он иначе, чем 404 контента выше:

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

Отличить его легко: есть поля error и statusCode, а message начинается с Cannot GET …. Это всегда ошибка в вашем коде, а не отсутствующий контент.

Частые причины:

  • Единственное число вместо множественного: /v1/page/home, /v1/block/phone.
  • Забытый /v1 в пути.
  • Лишние сегменты: /v1/pages/home/hero — у секций свой запрос, /v1/sections/hero.

Что делать: сверьте адрес со справочником API. Базовый адрес в одной константе (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 …» — неверный адрес
  | '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 покажет «что-то пошло не так»
  }
}