Buscar en la documentación

Módulo Shared

Piezas transversales de la API: excepciones de dominio, errores y identificador de petición.

6 min de lecturaVer en GitHub

Reúne lo que usan todos los módulos y no pertenece a ningún dominio en particular.

Excepciones de dominio

Las reglas de negocio no conocen HTTP: lanzan una excepción de dominio con un código estable y, si hace falta, los parámetros que necesita el mensaje.

ExcepciónCuándo usarlaRespuesta
ResourceNotFoundExceptionEl recurso no existe o quien consulta no puede verlo404
ConflictExceptionChoca con el estado actual (transición inválida, versión desactualizada)409
RuleViolationExceptionLos datos son válidos en forma pero rompen una regla de negocio422
AuthenticationFailedExceptionNo se pudo comprobar quién es la persona401
ForbiddenExceptionPuede ver el recurso, pero su rol no incluye esa acción. Por defecto FORBIDDEN_ACTION; un módulo puede dar un motivo más preciso (p. ej. USER_OUTRANKED)403
TemporarilyLockedExceptionAlgo quedó bloqueado por un tiempo; indica cuántos segundos faltan423, con Retry-After
throw new ConflictException('INVALID_STATUS_TRANSITION', ['from' => 'closed', 'to' => 'open']);

Cada módulo define sus propios códigos en su capa de dominio. La traducción a estado HTTP ocurre en un único lugar: el renderizador de errores.

Una excepción de dominio es una respuesta esperada (un código incorrecto, un conflicto, falta de permiso), no una falla: bootstrap/app.php la excluye del reporte de Laravel (dontReport), así no llena el log. Lo que sí es una falla (un 500) queda además como evento del sistema.

Errores de la API

Toda excepción que llega a la API, sea de negocio, de validación, del framework o inesperada, pasa por ApiExceptionRenderer y sale en formato RFC 9457 sin detalles técnicos. El formato completo y la lista de códigos están en Errores de la API.

Identificador de petición

Cada petición recibe un identificador que:

  • se devuelve en la cabecera X-Request-Id,
  • aparece como trace_id en los errores,
  • se agrega al contexto de todos los logs de esa petición.

Así, cuando alguien reporta un problema con su identificador, se encuentra en los logs sin exponer nada técnico. Si el cliente envía su propio X-Request-Id se reutiliza, pero solo si tiene un formato seguro (letras, números y guiones, de 8 a 64 caracteres); si no, se genera un ULID nuevo.

JSON en toda la API

Las peticiones a /api/* se tratan siempre como JSON, aunque el cliente no envíe la cabecera Accept. Por eso incluso una ruta inexistente responde con un error JSON y no con una página HTML.

Normalización de correos

EmailAddress::normalize() es la única regla para limpiar un correo: quita los espacios y lo pasa a minúsculas. La usan el modelo de usuario al guardar y el login al buscar, así ambos aplican siempre lo mismo.

Límite general de peticiones

Toda la API tiene un límite de 120 peticiones por minuto: por usuario si hay sesión, y por IP si no la hay. Lo define AppServiceProvider (trazo.api_rate_limit). En producción se queda en 120; la variable API_RATE_LIMIT solo se sube a mano para un escaneo o una prueba de carga en local. Las rutas sensibles, como el login, suman su propio límite, más estricto.

Dónde viven los contadores. Los límites de frecuencia (RateLimiter) y los bloqueos del login usan su propio almacén (cache.limiter = limits), en la base 2 de Redis (REDIS_LIMITS_DB), aparte de la caché de datos (base 1). Limpiar la caché de datos nunca levanta un bloqueo.

En desarrollo, php artisan trazo:reset-limits vacía solo esa base: la suite E2E lo usa al empezar para poder repetirse sin esperar 15 minutos. En producción el comando no se registra.

Carga diferida estricta

Fuera de producción, AppServiceProvider activa Model::preventLazyLoading(): si un código lee una relación que no se pidió con with(), Eloquent lanza una excepción en lugar de hacer una consulta más por cada registro. Así un N+1 nuevo falla en desarrollo y en la suite, aunque nadie haya escrito una prueba que cuente consultas para esa pantalla. En producción queda apagado, para que un caso no previsto se vuelva lento y no se caiga (ADR-0061).

Puertos que comparten los módulos

PiezaCapaPara qué
ClockApplicationLa hora actual, inyectada para que las reglas de fechas se puedan probar
TransactionApplicationVarias escrituras como una sola: o se guardan todas o ninguna
RequestContextApplicationDesde dónde se hizo un cambio (IP y navegador), para los registros de auditoría
LaravelSupportInfrastructureImplementa Clock y Transaction sobre Laravel; se conecta en AppServiceProvider
AdminLog, AdminActionTypeApplicationRegistro de acciones administrativas en admin_actions, con su tipo del catálogo
DatabaseAdminLogInfrastructureImplementa AdminLog; lee los tipos una vez por petición
SessionSignalsApplicationAvisa en vivo que la sesión de alguien cambió (Realtime)
SystemEvents, SystemEventTypeApplicationRegistro de eventos del sistema agrupados por huella; lo implementa el módulo System

Clock, Transaction y RequestContext vivían en el módulo de incidentes; al necesitarlos también Usuarios y el Dashboard, se movieron aquí para que ningún módulo dependa de otro por algo tan general. AdminLog vivía en Usuarios y se movió cuando la exportación de incidentes también tuvo que dejar constancia (ADR-0058); la fase de sistema registrará ahí sus acciones.

Cursor de listas

TimeCursor (Domain) es el “cargar más” de las listas que crecen sin parar: la fecha y el id del último registro entregado, en base64 sin relleno. La siguiente tanda empieza justo después, sin importar cuántos registros lleguen mientras tanto. La regla Http/Rules/Cursor rechaza uno alterado a mano (INVALID_CURSOR). Lo usan la auditoría y los eventos del sistema.

Descarga en CSV

CsvDownload (capa Http) arma una descarga en CSV que se escribe por partes, sin juntar las filas en memoria:

  • UTF-8 con BOM y punto y coma como separador, para que el Excel en español la abra bien.
  • Celdas entre comillas solo cuando hace falta, y líneas terminadas en \r\n.
  • CsvDownload::cell neutraliza la inyección de fórmulas: si una celda empieza por =, +, -, @, tabulador o salto de línea, le antepone un apóstrofo.
  • Sin caché (Cache-Control: no-store).

La usa la exportación de incidentes; cada módulo decide sus columnas y su formato.

Dónde está en el código