Diil Docs
  1. Documentación
  2. Guías

Errores: cada código y qué hacer con él

Actualizado:

Todos los errores que te puede devolver la Content API, en un solo sitio: qué pinta tiene el cuerpo, por qué suele pasar y cómo arreglarlo. La API es de solo lectura y bastante pequeña, así que la lista es corta, y cada error trae un message legible por máquina: tu código nunca tiene que adivinar.

Todos los errores de un vistazo

EstadomessageEn una frase
400invalid_markerEl marcador de la URL no es un marcador válido.
400invalid_langUn código de idioma en lang está mal formado.
400unknown_langEl código está bien, pero el sitio no tiene ese idioma activo.
400too_many_langsMás de 50 códigos en lang.
401invalid_keyLa clave de sitio falta, está mal formada o revocada.
404page_not_foundNo hay ninguna página con este marcador.
404section_not_foundNo hay ninguna sección con este marcador.
404block_not_foundNo hay ningún bloque con este marcador (en esta sección).
404post_not_foundNo hay ninguna entrada visible del blog con este slug.
404Cannot GET /v1/…La propia URL no existe en la API.
405method_not_allowedCualquier método que no sea GET o HEAD.
429rate_limit_exceededDemasiadas peticiones en este minuto.
5xx—Algo se ha roto en nuestro lado.

Qué pinta tiene un error

El cuerpo de cualquier error es un JSON con el campo message. Algunos errores añaden un par de campos útiles: por ejemplo, unknown_lang te dice qué idiomas tiene realmente el sitio.

Una respuesta de error típicahttp
HTTP/1.1 404 Not Found
Content-Type: application/json
Cache-Control: no-store

{"message":"page_not_found"}

Los errores siempre se envían con Cache-Control: no-store, así que un 404 de una página que un editor está creando justo ahora no se quedará atascado en la caché del navegador o de la CDN. Arregla la causa y la siguiente petición recibirá una respuesta normal.

400 Bad Request

La petición en sí está mal. Reintentar no sirve de nada: arregla la URL.

invalid_marker

400json
{"message":"invalid_marker"}

Los marcadores de páginas, secciones y bloques deben cumplir ^[A-Za-z0-9_]{2,40}$: letras latinas, dígitos y guiones bajos, de 2 a 40 caracteres.

Causas típicas:

  • Un guion o un punto: /v1/pages/about-us; los marcadores usan guiones bajos, about_us.
  • Una ruta en lugar de un marcador: el href de la página (/about) no es su marcador (about).
  • Un solo carácter, más de 40 caracteres, espacios o letras no latinas.
  • Un segmento de la URL del visitante pasado tal cual a la API: alguien escribió /über-uns en la barra de direcciones.

Cómo arreglarlo: copia el marcador desde el CRM. Si tus rutas usan guiones, tradúcelas a marcadores en tu router. Y cuando el marcador viene de la URL de un visitante, trata invalid_marker exactamente como un 404: esa página simplemente no existe.

invalid_lang

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

Cada código en lang debe tener la forma ^[a-z]{2}(-[A-Za-z]{2,4})?$: dos letras minúsculas, opcionalmente seguidas de una región como pt-BR. La comprobación distingue mayúsculas y minúsculas. El campo lang te devuelve el valor que no ha pasado.

Causas típicas:

  • Mayúsculas: EN en lugar de en.
  • Un locale con guion bajo sacado del servidor o de una librería de i18n: en_US.
  • Códigos de tres letras como eng, o el nombre completo como english.

Cómo arreglarlo: usa exactamente los códigos que tiene el sitio; salen de GET /v1/langs. Lo más fiable es normalizar lo que te dé tu framework a uno de esos códigos.

unknown_lang

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

El código está bien formado, pero no es un idioma activo de este sitio. Aquí el cuerpo es útil de verdad: unknown lista todos los códigos pedidos que han fallado y available, los que sí puedes usar.

Causas típicas:

  • Un editor desactivó un idioma en el CRM y tu código lo sigue pidiendo por su nombre.
  • El locale del navegador pasado tal cual: en-US tiene un formato válido, pero el sitio tiene en.
  • El código del vecino: ua donde el sitio usa uk, cz donde usa cs.
  • La clave pertenece a otro sitio con otro conjunto de idiomas.

Cómo arreglarlo: construye tu lista de idiomas a partir de GET /v1/langs en lugar de escribirla a mano, o recurre al primer código de available. Si de todas formas necesitas todos los idiomas, simplemente omite lang. Más en Idiomas y respaldo.

too_many_langs

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

Puedes pedir como máximo 50 códigos en una petición. Si te topas con esto, normalmente la lista se construye a partir de algo sin límite: una cabecera, lo que escribe el usuario, un bucle que no para de añadir.

Cómo arreglarlo: para obtener todos los idiomas activos, omite lang por completo: eso es justo lo que hace por defecto.

401 invalid_key

401json
{"message":"invalid_key"}

La API no ha aceptado la clave de sitio. Una clave válida es pk_ seguido de 32 caracteres hexadecimales en minúscula y se envía en la cabecera x-crm-key (o como ?key=).

Causas típicas:

  • Falta la cabecera, casi siempre porque la variable de entorno está vacía en este entorno.
  • En el navegador, la variable nunca llegó al bundle (en Next.js solo entran las variables NEXT_PUBLIC_*), así que la cabecera dice literalmente undefined.
  • Comillas, espacios o un salto de línea final copiados en .env junto con la clave.
  • La clave se revocó en el CRM.
  • La clave se creó hace un momento. Normalmente funciona al instante; en el peor de los casos puede tardar unos minutos en reconocerse. Lo mismo pasa con la revocación.

Cómo arreglarlo: revisa la clave en el CRM, en Ajustes → Claves de Content API, y pruébala con curl. Los detalles, en la página Claves de sitio.

¿Sigue viva esta clave?bash
curl -i "https://back.sitecog.com/content/v1/langs" -H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"

404 Not Found: ese contenido no existe

La URL está bien, la clave está bien, pero en esa dirección no hay nada. El message te dice qué es exactamente lo que falta:

messageEndpointSospechosos habituales
page_not_found/v1/pages/:markerUna errata o mayúsculas equivocadas (Home ≠ home), una página renombrada o borrada en el CRM, una clave de otro sitio.
section_not_found/v1/sections/:markerLo mismo que arriba. Ojo: las secciones ocultas (show: false) se devuelven igualmente, así que un 404 significa de verdad que la sección no está.
block_not_found/v1/blocks/:markerUna errata, o el bloque vive en otra sección distinta de la que pasaste en ?section=.
post_not_found/v1/blog/:slugLa entrada es un borrador, está programada para el futuro o el slug no es válido (solo a–z en minúscula, dígitos y guiones, hasta 120 caracteres; un slug no válido da 404, no 400).
404json
{"message":"page_not_found"}

Cómo arreglarlo: si el marcador está escrito a mano en tus plantillas, compáralo con el CRM. Si viene de la URL de un visitante, no es ningún bug: es un 404 normal y corriente, así que muestra tu página de «página no encontrada» (en Next.js, llama a notFound()).

Ruta desconocida

Si la URL no coincide con ningún endpoint, recibes el 404 estándar del framework, que tiene una pinta distinta de los 404 de contenido de arriba:

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

Lo distingues por los campos error y statusCode y por el mensaje Cannot GET …. Siempre es un bug de tu código, nunca contenido que falta.

Causas típicas:

  • Singular en lugar de plural: /v1/page/home, /v1/block/phone.
  • Se te ha olvidado /v1 en la ruta.
  • Segmentos de más: /v1/pages/home/hero; las secciones tienen su propio endpoint, /v1/sections/hero.

Cómo arreglarlo: compara la URL con la referencia de la API. Si guardas la URL base en una sola constante (https://back.sitecog.com/content) y construyes las rutas en un único helper, te ahorras la mayoría de estos fallos.

405 method_not_allowed

405json
{"message":"method_not_allowed"}

La Content API es de solo lectura: responde a GET y HEAD. POST, PUT, PATCH y DELETE reciben un 405.

Causas típicas: un cliente HTTP que usa POST por defecto, un formulario cuyo action apunta a la API, o la esperanza de guardar contenido a través de ella. El contenido se edita en el CRM o directamente en el sitio en modo Live; la API solo lee.

429 rate_limit_exceeded

429json
{"message":"rate_limit_exceeded"}

Demasiadas peticiones en el minuto actual: desde tu IP (300), con tu clave (600), o demasiados intentos con claves incorrectas desde tu IP (20). No hay cabecera Retry-After: espera al siguiente minuto, con un poco de jitter. Los límites, las cuentas y un helper de reintentos listo para usar están en la página Límites.

5xx: cosa nuestra

Un 500, 502, 503 o 504 significa que algo ha fallado en nuestro lado, no en tu código. Estos son los errores que sí vale la pena reintentar: un backoff exponencial corto con jitter (ejemplo) y, si sigue fallando, muestra la última copia buena que tengas en caché. Un sitio que sigue sirviendo el texto de ayer durante un tropiezo es muchísimo mejor que uno que enseña un stack trace.

Errores de red y CORS

A veces no hay respuesta en absoluto: fetch se rechaza con un TypeError («Failed to fetch», «fetch failed»). Causas:

  • Problemas de DNS, sin conexión, un firewall en tu servidor, una petición que se queda colgada. Pon un timeout: AbortSignal.timeout(10_000).
  • CORS en el navegador. La API admite cualquier origen, los métodos GET y OPTIONS y las cabeceras de petición x-crm-key, content-type e if-none-match. Añade cualquier otra cabecera personalizada (un Authorization, una cabecera de trazas de tu cliente HTTP) y el navegador bloqueará la petición antes de enviarla: tu código lo verá como un error de red. Mira la consola: el navegador te dice qué cabecera no le ha gustado.

Un manejador de errores robusto en TypeScript

Mete todo lo anterior en un pequeño módulo: cada respuesta que no sea 2xx y cada fallo de red se convierte en un ContentApiError tipado con el estado, el código legible por máquina y los campos extra. El resto de tu código solo tiene que mirar 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 …": la URL está mal
  | 'server_error'    // 5xx
  | 'network_error'   // ninguna respuesta
  | 'unexpected';     // cualquier cosa que no hayamos visto antes

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;          // estado HTTP, 0 si no hubo respuesta
  readonly code: ContentErrorCode;
  readonly lang?: string;           // invalid_lang, unknown_lang
  readonly unknown?: string[];      // unknown_lang: los códigos que han fallado
  readonly available?: string[];    // unknown_lang: los códigos que puedes usar
  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;
  }

  /** Vale la pena reintentar más tarde: problemas de red, límite de peticiones, nuestros 5xx */
  get retryable() {
    return this.status === 0 || this.status === 429 || this.status >= 500;
  }

  /** El contenido no existe: muestra tu página 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;

  // Los cuerpos de error son JSON, pero un proxy por el camino podría responder con HTML: cuidado
  const body: ErrorBody = await res.json().catch(() => ({}));
  throw new ContentApiError(res.status, toCode(res.status, body.message), body);
}

Y así se lee en código real:

// 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…: un bug que arreglar
    }
    throw err; // error.tsx muestra «algo ha ido mal»
  }
}