Alle Fehler, die dir die Content API schicken kann, an einem Ort: wie der Body aussieht, woher sie meistens kommen und wie du sie behebst. Die API ist read-only und recht überschaubar, also ist die Liste kurz — und jeder Fehler kommt mit einer maschinenlesbaren message, damit dein Code nie raten muss.
Alle Fehler auf einen Blick
| Status | message | In einem Satz |
|---|---|---|
| 400 | invalid_marker | Der Marker in der URL ist kein gültiger Marker. |
| 400 | invalid_lang | Ein Sprachcode in lang ist falsch formatiert. |
| 400 | unknown_lang | Der Sprachcode ist korrekt, aber die Website hat keine solche aktive Sprache. |
| 400 | too_many_langs | Mehr als 50 Codes in lang. |
| 401 | invalid_key | Der Site-Key fehlt, ist falsch formatiert oder wurde widerrufen. |
| 404 | page_not_found | Keine Seite mit diesem Marker. |
| 404 | section_not_found | Keine Section mit diesem Marker. |
| 404 | block_not_found | Kein Block mit diesem Marker (in dieser Section). |
| 404 | post_not_found | Kein sichtbarer Blogpost mit diesem Slug. |
| 404 | Cannot GET /v1/… | Die URL selbst gibt es in der API nicht. |
| 405 | method_not_allowed | Alles außer GET oder HEAD. |
| 429 | rate_limit_exceeded | Zu viele Requests in dieser Minute. |
| 5xx | — | Bei uns ist etwas kaputtgegangen. |
So sieht ein Fehler aus
Jeder Fehler-Body ist JSON mit einem Feld message. Manche Fehler legen noch ein paar nützliche Felder drauf — unknown_lang verrät dir zum Beispiel, welche Sprachen die Website wirklich hat.
HTTP/1.1 404 Not Found
Content-Type: application/json
Cache-Control: no-store
{"message":"page_not_found"}Fehler kommen immer mit Cache-Control: no-store. Ein 404 für eine Seite, die eine Redakteurin gerade anlegt, bleibt also nicht im Browser- oder CDN-Cache hängen. Ursache beheben — und der nächste Request bekommt eine ganz normale Antwort.
400 Bad Request
Der Request selbst ist falsch. Nochmal versuchen bringt nichts — korrigiere die URL.
invalid_marker
{"message":"invalid_marker"}Marker von Seiten, Sections und Blöcken müssen auf ^[A-Za-z0-9_]{2,40}$ passen: lateinische Buchstaben, Ziffern und Unterstriche, 2 bis 40 Zeichen.
Typische Ursachen:
- Ein Bindestrich oder ein Punkt:
/v1/pages/about-us— Marker nutzen Unterstriche,about_us. - Ein Pfad statt eines Markers: das
hrefder Seite (/about) ist nicht ihr Marker (about). - Ein einzelnes Zeichen, mehr als 40 Zeichen, Leerzeichen oder nicht-lateinische Buchstaben.
- Ein URL-Segment vom Besucher, direkt an die API durchgereicht — jemand hat
/über-unsin die Adresszeile getippt.
So behebst du es: kopier den Marker aus dem CRM. Wenn deine Routen Bindestriche nutzen, mappe sie im Router auf Marker. Und wenn ein Marker aus der URL eines Besuchers kommt, behandle invalid_marker genau wie einen 404: Diese Seite gibt es schlicht nicht.
invalid_lang
{"message":"invalid_lang","lang":"EN"}Jeder Code in lang muss so aussehen: ^[a-z]{2}(-[A-Za-z]{2,4})?$ — zwei Kleinbuchstaben, optional gefolgt von einer Region wie pt-BR. Groß- und Kleinschreibung zählt. Das Feld lang gibt den Wert zurück, der durchgefallen ist.
Typische Ursachen:
- Großbuchstaben:
ENstatten. - Eine Locale mit Unterstrich vom Server oder aus einer i18n-Library:
en_US. - Dreibuchstabige Codes wie
engoder ein voller Name wieenglish.
So behebst du es: nimm genau die Codes, die die Website hat — sie kommen von GET /v1/langs. Am zuverlässigsten ist es, das, was dein Framework dir liefert, auf einen dieser Codes zu normalisieren.
unknown_lang
{
"message": "unknown_lang",
"lang": "fr",
"unknown": ["fr"],
"available": ["en", "de"]
}Der Code ist korrekt aufgebaut, aber keine aktive Sprache dieser Website. Der Body ist hier richtig nützlich: unknown listet jeden angefragten Code, der durchgefallen ist, available die, die du verwenden kannst.
Typische Ursachen:
- Jemand hat eine Sprache im CRM abgeschaltet, und dein Code fragt sie immer noch namentlich an.
- Die Browser-Locale wird ungefiltert durchgereicht:
en-USist ein gültiges Format, aber die Website haten. - Der Code des Nachbarlandes:
ua, wo die Websiteuknutzt,czstattcs. - Der Key gehört zu einer anderen Website mit anderen Sprachen.
So behebst du es: bau deine Sprachliste aus GET /v1/langs auf, statt sie hart zu codieren, oder fall auf den ersten Code aus available zurück. Wenn du sowieso alle Sprachen brauchst, lass lang einfach weg. Mehr dazu unter Sprachen & Fallbacks.
too_many_langs
{"message":"too_many_langs","max":50}Pro Request gehen höchstens 50 Codes. Wer hier landet, baut die Liste meistens aus etwas Unbegrenztem — einem Header, einer Benutzereingabe, einer Schleife, die munter weiter anhängt.
So behebst du es: für alle aktiven Sprachen lass lang komplett weg — genau das passiert dann standardmäßig.
401 invalid_key
{"message":"invalid_key"}Die API hat den Site-Key nicht akzeptiert. Ein gültiger Key besteht aus pk_ plus 32 Hex-Zeichen in Kleinbuchstaben und wird im Header x-crm-key (oder als ?key=) mitgeschickt.
Typische Ursachen:
- Der Header fehlt — meistens, weil die Umgebungsvariable in dieser Umgebung leer ist.
- Im Browser hat es die Variable nie ins Bundle geschafft (in Next.js schaffen das nur
NEXT_PUBLIC_*-Variablen), also steht im Header wortwörtlichundefined. - Anführungszeichen, Leerzeichen oder ein Zeilenumbruch am Ende sind zusammen mit dem Key in der
.envgelandet. - Der Key wurde im CRM widerrufen.
- Der Key wurde gerade eben erstellt. Normalerweise funktioniert er sofort; im schlimmsten Fall dauert es ein paar Minuten, bis er erkannt wird. Dasselbe gilt fürs Widerrufen.
So behebst du es: prüf den Key im CRM unter Einstellungen → Content-API-Keys und teste ihn mit curl. Details auf der Seite Site-Keys.
curl -i "https://back.sitecog.com/content/v1/langs" -H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"404 Not Found: Inhalt gibt es nicht
Die URL passt, der Key passt, aber unter dieser Adresse ist nichts. Die message sagt dir, was genau fehlt:
| message | Endpoint | Die üblichen Verdächtigen |
|---|---|---|
page_not_found | /v1/pages/:marker | Ein Tippfehler oder falsche Schreibweise (Home ≠ home), eine im CRM umbenannte oder gelöschte Seite, ein Key einer anderen Website. |
section_not_found | /v1/sections/:marker | Wie oben. Ausgeblendete Sections (show: false) werden übrigens trotzdem geliefert — ein 404 heißt wirklich, dass die Section nicht existiert. |
block_not_found | /v1/blocks/:marker | Ein Tippfehler, oder der Block liegt in einer anderen Section als der, die du in ?section= übergeben hast. |
post_not_found | /v1/blog/:slug | Der Post ist ein Entwurf, für später geplant, oder der Slug ist ungültig (nur kleine a–z, Ziffern und Bindestriche, bis 120 Zeichen — ein ungültiger Slug ergibt 404, nicht 400). |
{"message":"page_not_found"}So behebst du es: wenn der Marker fest in deinen Templates steht, gleich ihn mit dem CRM ab. Kommt er aus der URL eines Besuchers, ist das gar kein Bug — sondern ein ganz normaler 404. Zeig also deine „Seite nicht gefunden“-Seite (in Next.js: notFound() aufrufen).
Unbekannte Route
Passt die URL zu keinem Endpoint, bekommst du den Standard-404 des Frameworks, und der sieht anders aus als die Content-404er oben:
{
"message": "Cannot GET /v1/page/home",
"error": "Not Found",
"statusCode": 404
}Du erkennst ihn an den Feldern error und statusCode und an der Message Cannot GET …. Das ist immer ein Bug in deinem Code, nie fehlender Inhalt.
Typische Ursachen:
- Singular statt Plural:
/v1/page/home,/v1/block/phone. - Ein vergessenes
/v1im Pfad. - Zusätzliche Segmente:
/v1/pages/home/hero— Sections haben ihren eigenen Endpoint,/v1/sections/hero.
So behebst du es: vergleich die URL mit der API-Referenz. Wenn die Base-URL in einer Konstante liegt (https://back.sitecog.com/content) und Pfade in einem einzigen Helper gebaut werden, passiert das meiste davon gar nicht erst.
405 method_not_allowed
{"message":"method_not_allowed"}Die Content API ist read-only: Sie antwortet auf GET und HEAD. POST, PUT, PATCH und DELETE bekommen einen 405.
Typische Ursachen: ein HTTP-Client, der standardmäßig POST schickt, ein Formular, dessen action auf die API zeigt, oder die Hoffnung, darüber Inhalte zu speichern. Inhalte bearbeitet man im CRM oder direkt auf der Website im Live-Modus — die API liest nur.
429 rate_limit_exceeded
{"message":"rate_limit_exceeded"}Zu viele Requests in der aktuellen Minute — von deiner IP (300), mit deinem Key (600) oder zu viele Versuche mit falschem Key von deiner IP (20). Einen Retry-After-Header gibt es nicht: Warte bis zur nächsten Minute, mit ein bisschen Jitter. Limits, Rechnung und einen fertigen Retry-Helper findest du auf der Seite Rate Limits.
5xx: unsere Seite
Ein 500, 502, 503 oder 504 heißt: Bei uns ist etwas schiefgelaufen, nicht in deinem Code. Das sind die Fehler, bei denen sich ein Retry lohnt: kurzer exponentieller Backoff mit Jitter (Beispiel), und wenn es dann immer noch nicht klappt, zeig die letzte gute Kopie aus deinem Cache. Eine Website, die während eines Schluckaufs den Text von gestern zeigt, ist viel besser als eine, die einen Stacktrace zeigt.
Netzwerkfehler und CORS
Manchmal kommt gar keine Response — fetch wirft einen TypeError („Failed to fetch“, „fetch failed“). Ursachen:
- DNS-Probleme, keine Verbindung, eine Firewall auf deinem Server, ein Request, der hängt. Setz einen Timeout:
AbortSignal.timeout(10_000). - CORS im Browser. Die API erlaubt jede Origin, die Methoden
GETundOPTIONSund die Request-Headerx-crm-key,content-typeundif-none-match. Kommt irgendein anderer eigener Header dazu (einAuthorization, ein Tracing-Header von deinem HTTP-Client), blockiert der Browser den Request, bevor er überhaupt rausgeht — und dein Code sieht einen Netzwerkfehler. Schau in die Konsole: Der Browser verrät dir, welcher Header ihm nicht gepasst hat.
Ein robuster Error-Handler in TypeScript
Pack alles oben Genannte in ein kleines Modul: Jede Nicht-2xx-Response und jeder Netzwerkfehler wird zu einem typisierten ContentApiError mit Status, maschinenlesbarem Code und den Zusatzfeldern. Der Rest deines Codes prüft nur noch 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 …" — die URL ist falsch
| 'server_error' // 5xx
| 'network_error' // gar keine Response
| 'unexpected'; // alles, was wir noch nie gesehen haben
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-Status, 0 wenn keine Response kam
readonly code: ContentErrorCode;
readonly lang?: string; // invalid_lang, unknown_lang
readonly unknown?: string[]; // unknown_lang: die durchgefallenen Codes
readonly available?: string[]; // unknown_lang: die Codes, die du nutzen kannst
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;
}
/** Lohnt einen späteren Versuch: Netzwerkprobleme, Rate Limit, unser 5xx */
get retryable() {
return this.status === 0 || this.status === 429 || this.status >= 500;
}
/** Den Inhalt gibt es nicht — zeig deine 404-Seite */
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;
// Fehler-Bodies sind JSON, aber ein Proxy unterwegs antwortet vielleicht mit HTML — also vorsichtig
const body: ErrorBody = await res.json().catch(() => ({}));
throw new ContentApiError(res.status, toCode(res.status, body.message), body);
}Und so liest sich das im echten Code:
// 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… — ein Bug, den du fixen solltest
}
throw err; // error.tsx zeigt „etwas ist schiefgelaufen“
}
}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 oder kein Netz: Wir zeigen weiter, was wir schon haben
console.warn('Content API is having a moment:', err.code);
} else {
throw err;
}
}
return lastGood;
}