Saltearse al contenido

Errores

La API responde con el código HTTP y un cuerpo JSON pequeño. El código manda; message es prosa y puede cambiar. La excepción es el 422: sus claves son nombres de campo y forman parte del contrato.

Código Cuerpo
401 {"message": "..."}
403 {"message": "..."} y, según el caso, module, ability, code o access
404 {"message": "..."}
422 {"errors": {"campo": ["mensaje", ...]}}
429 {"message": "..."} y cabeceras de límite

Controller::invalid() arma el 422 con solo errors: no cuentes con un message ahí.

Falta el token, venció o no es válido: {"message":"Unauthenticated."}. Dos variantes: el login con credenciales incorrectas devuelve 401 con Credenciales inválidas.; un Bearer que no resuelve en POST /api/leads o POST /api/track devuelve 401 con Token inválido.. Sin ninguna credencial, /leads responde 422 y pide site_key.

No reintentes con el mismo token: cambia la credencial.

  • Módulo. El sitio no tiene el módulo habilitado o su plan no se lo permite:
{"message":"El módulo 'blog' no está habilitado para este sitio.","module":"blog"}
  • Habilidad. El token de sitio no tiene la habilidad que la ruta exige:
{"message":"El token no tiene la habilidad 'blog'.","ability":"blog"}
  • Plan o sitio. Una escritura con el plan vencido trae code (plan_read_only, plan_suspended) y access; la API de token de un sitio suspendido trae code: site_suspended.
  • Perfil o portal. Un miembro sin permiso recibe Tu perfil no permite…; un token del área de cliente no abre el panel.

Un recurso de otro sitio no da 403: da 404. La API no confirma que exista.

Puede significar que el recurso no existe, que es de otro sitio o que la ruta no existe. Esto último también aplica bajo /api/v1: una ruta inventada es 404, no un hueco.

El message no es estable. En el sitio público por slug sí es explícito (No existe ese sitio.), pero no lo uses como discriminador: decide por el código.

Es validación. Las claves son los campos de la regla y cada valor es una lista de mensajes:

{"errors":{"email":["<mensaje de la regla>"],"password":["<mensaje de la regla>"]}}

También hay 422 semánticos: falta la clave de ingest, la clave es inválida, el origen no está autorizado, el sitio está inactivo o intentas revocar el token con el que llamas.

El limitador general es de 120 por minuto por usuario o IP. Encima hay límites por ruta: POST /api/leads 30 por minuto, POST /api/track 120 por minuto, el login 5 por minuto por IP y correo (30 por IP) y la verificación de certificados 20 por minuto por documento e IP.

Laravel devuelve {"message":"Too Many Attempts."} con Retry-After y X-RateLimit-*. POST /api/leads suma su propio límite: 15 por hora por (sitio, IP); 100 por hora por sitio cuando no hay IP. Ese 429 es un cuerpo propio, sin esas cabeceras.

El frontend usa la clave para pintar el mensaje debajo del input. Al renombrar la regla contenido a content, errors.contenido desapareció y el formulario se quedó mudo: el 422 llegaba igual. Ningún test que compruebe el código de estado lo ve; lo caza AdditiveContractTest, porque la clave es parte de la forma grabada.

Por eso los controladores usan $this->invalid($validator): las claves salen de las reglas.

// 401
{"message":"Unauthenticated."}
// 422
{"errors":{"consent_granted":["Este sitio exige la autorización de tratamiento de datos."]}}
// 429
{"message":"Too Many Attempts."}

En el cliente, decide por el estado: el 422 se pinta campo a campo; el 401 y el 403 piden otra credencial o otro permiso; el 429 espera el Retry-After.

  • Renombrar una clave del 422. El mensaje deja de aparecer bajo el input, aunque el error siga llegando con el mismo código.
  • Parsear message. Es prosa y cambia. Usa el código de estado y, para el 422, las claves de errors.
  • Reintentar un 401 o un 403 tal cual. No se arreglan solos: falta credencial, habilidad, módulo o plan.
  • Ignorar Retry-After. Reintentar en bucle solo alarga el bloqueo.
  • Asumir que todo 422 trae message. invalid() no lo incluye.
  • Leer un 404 como «no existe». Puede ser «no es tuyo» o «ruta mal escrita», también bajo /api/v1.