Усі помилки, які може повернути Content API, в одному місці: як виглядає тіло відповіді, чому вона зазвичай виникає і як її виправити. API лише читає дані й доволі компактний, тож список короткий — а кожна помилка має машиночитний message, щоб вашому коду не доводилося нічого вгадувати.
Усі помилки одним поглядом
| Статус | message | Одним реченням |
|---|---|---|
| 400 | invalid_marker | Маркер в URL — некоректний. |
| 400 | invalid_lang | Код мови в lang має неправильний формат. |
| 400 | unknown_lang | Код мови правильний, але активної мови з таким кодом на сайті немає. |
| 400 | too_many_langs | Понад 50 кодів у lang. |
| 401 | invalid_key | Ключа сайту немає, він некоректний або відкликаний. |
| 404 | page_not_found | Сторінки з таким маркером немає. |
| 404 | section_not_found | Секції з таким маркером немає. |
| 404 | block_not_found | Блоку з таким маркером немає (у цій секції). |
| 404 | post_not_found | Немає опублікованого допису блогу з таким slug. |
| 404 | Cannot GET /v1/… | Такої адреси в API взагалі не існує. |
| 405 | method_not_allowed | Будь-який метод, окрім GET чи HEAD. |
| 429 | rate_limit_exceeded | Забагато запитів за цю хвилину. |
| 5xx | — | Щось зламалося на нашому боці. |
Як виглядає помилка
Тіло будь-якої помилки — це JSON із полем message. Деякі помилки додають кілька корисних полів — наприклад, unknown_lang підкаже, які мови на сайті є насправді.
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
{"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
{"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
{
"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
{"message":"too_many_langs","max":50}За один запит можна попросити щонайбільше 50 кодів. Якщо ви вперлися в це обмеження, найімовірніше, список будується з чогось необмеженого — заголовка, введення користувача, циклу, який усе додає й додає.
Як виправити: щоб отримати всі активні мови, просто не передавайте lang — саме це він і робить за замовчуванням.
401 invalid_key
{"message":"invalid_key"}API не прийняв ключ сайту. Коректний ключ — це pk_ і 32 малі шістнадцяткові символи; його передають у заголовку x-crm-key (або як ?key=).
Типові причини:
- Заголовка немає — найчастіше тому, що змінна оточення в цьому середовищі порожня.
- У браузері змінна так і не потрапила в бандл (у Next.js туди потрапляють лише змінні
NEXT_PUBLIC_*), тож у заголовку буквально написаноundefined. - Разом із ключем у
.envскопіювалися лапки, пробіли чи перенесення рядка в кінці. - Ключ відкликали в CRM.
- Ключ створили щойно. Зазвичай він працює одразу, але в гіршому разі розпізнавання може зайняти кілька хвилин. Те саме стосується відкликання.
Як виправити: перевірте ключ у CRM у розділі Налаштування → Ключі Content API і протестуйте його через curl. Подробиці — на сторінці Ключ сайту.
curl -i "https://back.sitecog.com/content/v1/langs" -H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"404 Not Found: такого вмісту немає
URL правильний, ключ правильний, але за цією адресою нічого немає. message підкаже, чого саме бракує:
| message | Endpoint | Звичні підозрювані |
|---|---|---|
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). |
{"message":"page_not_found"}Як виправити: якщо маркер прописано у ваших шаблонах, звірте його з CRM. Якщо він приходить з URL відвідувача, це взагалі не баг — це звичайна 404, тож покажіть свою сторінку «не знайдено» (у Next.js — викличте notFound()).
Невідомий маршрут
Якщо URL не відповідає жодному endpoint, ви отримаєте стандартну 404 фреймворку, яка виглядає інакше, ніж 404 для вмісту вище:
{
"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
{"message":"method_not_allowed"}Content API лише читає: він відповідає на GET і HEAD. POST, PUT, PATCH і DELETE отримують 405.
Типові причини: HTTP-клієнт, що за замовчуванням шле POST, форма, чий action вказує на API, або надія зберігати через нього вміст. Вміст редагують у CRM чи просто на сайті в режимі Live — API лише читає.
429 rate_limit_exceeded
{"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.
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 покаже «щось пішло не так»
}
}import { contentFetch, ContentApiError } from './content-api';
let lastGood: unknown = null;
export async function loadHome() {
try {
lastGood = await contentFetch('/v1/pages/home?lang=en');
} catch (err) {
if (err instanceof ContentApiError && err.retryable && lastGood) {
// 429, 5xx або немає мережі: далі показуємо те, що вже маємо
console.warn('Content API тимчасово не в настрої:', err.code);
} else {
throw err;
}
}
return lastGood;
}