All API errors follow a standardized JSON format for consistent client-side error handling.
Standard Error Structure
Every error response includes four fields:
{
"timestamp" : "2024-02-15T19:00:00Z" ,
"requestId" : "123e4567-e89b-12d3-a456-426614174000" ,
"message" : "Validation failed" ,
"detail" : "email: debe ser una dirección de correo electrónico con formato correcto"
}
Field Type Description timestampISO 8601 DateTime When the error occurred (UTC) requestIdUUID Unique identifier for tracing the request in logs messageString High-level error category or message detailString Specific error details or exception class name
ErrorResponse Implementation
The error response is implemented as a Java record (ErrorResponse.java:7-18):
public record ErrorResponse (
Instant timestamp,
String requestId,
String message,
String detail
) {
public static ErrorResponse of ( String message , String detail ) {
String reqId = MDC . get ( RequestIdFilter . MDC_KEY );
return new ErrorResponse ( Instant . now (), reqId, message, detail);
}
}
The requestId is automatically extracted from the Mapped Diagnostic Context (MDC), which is populated by the RequestIdFilter for every incoming request.
Always include the requestId when reporting issues. This allows developers to trace the exact request in server logs.
HTTP Status Codes
The service uses standard HTTP status codes to indicate the type of error:
400 Bad Request
Cause : Invalid input data or validation failures.
Common scenarios :
Missing required fields
Invalid email format
Password too short
Invalid date formats
Type mismatches
Example response :
{
"timestamp" : "2024-03-04T14:30:00Z" ,
"requestId" : "a1b2c3d4-e5f6-7890-abcd-ef1234567890" ,
"message" : "Validation failed" ,
"detail" : "password: size must be between 8 and 128; email: must be a well-formed email address"
}
Handler (GlobalExceptionHandler.java:39-50):
@ ExceptionHandler ( MethodArgumentNotValidException . class )
public ResponseEntity < ErrorResponse > handleMethodArgNotValid ( MethodArgumentNotValidException ex) {
String details = ex . getBindingResult ()
. getFieldErrors ()
. stream ()
. map (fe -> fe . getField () + ": " + fe . getDefaultMessage ())
. collect ( Collectors . joining ( ";" ));
ErrorResponse dto = ErrorResponse . of ( "Validation failed" , details);
return ResponseEntity . status ( HttpStatus . BAD_REQUEST ). body (dto);
}
401 Unauthorized
Cause : Authentication failure - missing, invalid, or expired JWT token.
Common scenarios :
No Authorization header provided
Invalid JWT signature (wrong secret)
Token expired
Malformed token
Invalid credentials on login
Example response :
{
"timestamp" : "2024-03-04T14:35:00Z" ,
"requestId" : "b2c3d4e5-f6a7-8901-bcde-f12345678901" ,
"message" : "Full authentication is required to access this resource" ,
"detail" : "AuthenticationException"
}
Handler (GlobalExceptionHandler.java:96-101):
@ ExceptionHandler ( AuthenticationException . class )
public ResponseEntity < ErrorResponse > handleAuthentication ( AuthenticationException ex) {
log . warn ( "Autenticación fallida: {}" , ex . getMessage ());
ErrorResponse dto = ErrorResponse . of ( ex . getMessage (), ex . getClass (). getSimpleName ());
return ResponseEntity . status ( HttpStatus . UNAUTHORIZED ). body (dto);
}
403 Forbidden
Cause : Authenticated but insufficient permissions to perform the action.
Common scenarios :
User lacks required role (e.g., ADMIN)
User lacks specific permission (e.g., USER_DELETE)
Attempting to modify resources owned by another user
Example response :
{
"timestamp" : "2024-03-04T14:40:00Z" ,
"requestId" : "c3d4e5f6-a7b8-9012-cdef-123456789012" ,
"message" : "Access is denied" ,
"detail" : "AccessDeniedException"
}
Handler (GlobalExceptionHandler.java:103-108):
@ ExceptionHandler ( AccessDeniedException . class )
public ResponseEntity < ErrorResponse > handleAccessDenied ( AccessDeniedException ex) {
log . warn ( "Acceso denegado: {}" , ex . getMessage ());
ErrorResponse dto = ErrorResponse . of ( ex . getMessage (), ex . getClass (). getSimpleName ());
return ResponseEntity . status ( HttpStatus . FORBIDDEN ). body (dto);
}
404 Not Found
Cause : Requested resource does not exist.
Common scenarios :
User ID not found
Role name not found
Permission not found
Module not found
Example response :
{
"timestamp" : "2024-03-04T14:45:00Z" ,
"requestId" : "d4e5f6a7-b8c9-0123-def0-234567890123" ,
"message" : "User with ID 999 not found" ,
"detail" : "UserNotFoundException"
}
Handler (GlobalExceptionHandler.java:82-87):
@ ExceptionHandler ({ UserNotFoundException . class , RoleNotFoundException . class ,
PermissionNotFoundException . class , ModuleNotFoundException . class })
public ResponseEntity < ErrorResponse > handleNotFound ( RuntimeException ex) {
log . info ( "No encontrado: {}" , ex . getMessage ());
ErrorResponse dto = ErrorResponse . of ( ex . getMessage (), ex . getClass (). getSimpleName ());
return ResponseEntity . status ( HttpStatus . NOT_FOUND ). body (dto);
}
409 Conflict
Cause : Request conflicts with existing data.
Common scenarios :
Email already registered
Role name already exists
Permission name already exists
Duplicate unique constraint violation
Example response :
{
"timestamp" : "2024-03-04T14:50:00Z" ,
"requestId" : "e5f6a7b8-c9d0-1234-ef01-345678901234" ,
"message" : "User with email john@example.com already exists" ,
"detail" : "UserAlreadyExistsException"
}
Handler (GlobalExceptionHandler.java:89-94):
@ ExceptionHandler ({ UserAlreadyExistsException . class , RoleAlreadyExistsException . class ,
PermissionAlreadyExistsException . class })
public ResponseEntity < ErrorResponse > handleConflict ( RuntimeException ex) {
log . info ( "Conflicto: {}" , ex . getMessage ());
ErrorResponse dto = ErrorResponse . of ( ex . getMessage (), ex . getClass (). getSimpleName ());
return ResponseEntity . status ( HttpStatus . CONFLICT ). body (dto);
}
500 Internal Server Error
Cause : Unexpected server-side error.
Common scenarios :
Database connection failure
Null pointer exceptions
Mapping errors
Uncaught exceptions
Example response :
{
"timestamp" : "2024-03-04T14:55:00Z" ,
"requestId" : "f6a7b8c9-d0e1-2345-f012-456789012345" ,
"message" : "Failed to connect to database" ,
"detail" : "PersistenceException"
}
Handlers :
// Persistence errors
@ ExceptionHandler ( PersistenceException . class )
public ResponseEntity < ErrorResponse > handlePersistence ( PersistenceException ex) {
log . error ( "Excepción de persistencia: {}" , ex . getMessage (), ex);
ErrorResponse dto = ErrorResponse . of ( ex . getMessage (), ex . getClass (). getSimpleName ());
return ResponseEntity . status ( HttpStatus . INTERNAL_SERVER_ERROR ). body (dto);
}
// Generic fallback
@ ExceptionHandler ( Exception . class )
public ResponseEntity < ErrorResponse > handleGeneric ( Exception ex) {
log . error ( "Excepción no controlada: {}" , ex . getMessage (), ex);
ErrorResponse dto = ErrorResponse . of ( ex . getMessage (), ex . getClass (). getSimpleName ());
return ResponseEntity . status ( HttpStatus . INTERNAL_SERVER_ERROR ). body (dto);
}
500 errors indicate a bug or configuration problem. Always check server logs using the requestId to diagnose the root cause.
Exception Hierarchy
The service defines custom domain exceptions for business logic errors:
Domain Exceptions
Located in com.autorization.autorization.auth.domain.exception:
UserNotFoundException
UserAlreadyExistsException
RoleNotFoundException
RoleAlreadyExistsException
PermissionNotFoundException
PermissionAlreadyExistsException
ModuleNotFoundException
Infrastructure Exceptions
Located in com.autorization.autorization.shared.domain.exception:
PersistenceException - Database operation failures
NullValueException - Unexpected null values
MappingException - Entity-to-DTO conversion errors (in adapter layer)
Security Exceptions
Located in com.autorization.autorization.security.exception:
TokenGenerationException - JWT generation failures
Validation Errors
Field-Level Validation
The service uses Jakarta Validation (formerly Bean Validation) with annotations:
public record CreateUserRequest (
@ NotBlank ( message = "Name is required" )
String name,
@ NotBlank ( message = "Email is required" )
@ Email ( message = "Must be a valid email address" )
String email,
@ NotBlank ( message = "Password is required" )
@ Size ( min = 8 , max = 128 , message = "Password must be between 8 and 128 characters" )
String password
) {}
Validation errors are aggregated and returned in a single response:
{
"timestamp" : "2024-03-04T15:00:00Z" ,
"requestId" : "a7b8c9d0-e1f2-3456-0123-567890123456" ,
"message" : "Validation failed" ,
"detail" : "name: Name is required; email: Must be a valid email address; password: Password must be between 8 and 128 characters"
}
Constraint Violations
Database constraints (unique, foreign key, check) are caught and returned as validation errors:
Handler (GlobalExceptionHandler.java:52-62):
@ ExceptionHandler ( ConstraintViolationException . class )
public ResponseEntity < ErrorResponse > handleConstraintViolation ( ConstraintViolationException ex) {
String details = ex . getConstraintViolations ()
. stream ()
. map (cv -> cv . getPropertyPath () + ": " + cv . getMessage ())
. collect ( Collectors . joining ( "; " ));
ErrorResponse dto = ErrorResponse . of ( "Validation failed" , details);
return ResponseEntity . status ( HttpStatus . BAD_REQUEST ). body (dto);
}
Client-Side Error Handling
JavaScript/TypeScript Example
interface ErrorResponse {
timestamp : string ;
requestId : string ;
message : string ;
detail : string ;
}
async function createUser ( userData : CreateUserRequest ) {
try {
const response = await fetch ( '/api/users' , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json' ,
'Authorization' : `Bearer ${ token } `
},
body: JSON . stringify ( userData )
});
if ( ! response . ok ) {
const error : ErrorResponse = await response . json ();
switch ( response . status ) {
case 400 :
// Show validation errors to user
showValidationErrors ( error . detail );
break ;
case 401 :
// Redirect to login
redirectToLogin ();
break ;
case 403 :
// Show permission denied message
showError ( 'You do not have permission to perform this action' );
break ;
case 409 :
// Show conflict error (e.g., email already exists)
showError ( error . message );
break ;
case 500 :
// Log error and show generic message
console . error ( `Server error: ${ error . requestId } ` );
showError ( 'An unexpected error occurred. Please try again later.' );
break ;
default :
showError ( 'An error occurred' );
}
return null ;
}
return await response . json ();
} catch ( error ) {
// Network error or JSON parsing error
console . error ( 'Request failed:' , error );
showError ( 'Unable to connect to the server' );
return null ;
}
}
React Hook Example
import { useState } from 'react' ;
function useApiError () {
const [ error , setError ] = useState < ErrorResponse | null >( null );
const handleError = ( error : ErrorResponse , status : number ) => {
setError ( error );
// Log to monitoring service (e.g., Sentry)
if ( status === 500 ) {
logErrorToMonitoring ({
requestId: error . requestId ,
message: error . message ,
detail: error . detail ,
timestamp: error . timestamp
});
}
};
const clearError = () => setError ( null );
return { error , handleError , clearError };
}
Debugging with Request IDs
Finding Errors in Logs
The requestId is included in all log messages through MDC (Mapped Diagnostic Context):
Search server logs :
grep "a7b8c9d0-e1f2-3456-0123-567890123456" logs/application.log
Example log output :
2024-03-04 15:00:00.123 [http-nio-8080-exec-1] WARN [a7b8c9d0-e1f2-3456-0123-567890123456] c.a.a.s.i.w.GlobalExceptionHandler - Validación de parámetros fallida: email: Must be a valid email address
Request ID Filter
The RequestIdFilter automatically generates a unique ID for each request:
@ Component
public class RequestIdFilter extends OncePerRequestFilter {
public static final String MDC_KEY = "requestId" ;
@ Override
protected void doFilterInternal ( HttpServletRequest request ,
HttpServletResponse response ,
FilterChain filterChain ) {
String requestId = UUID . randomUUID (). toString ();
MDC . put (MDC_KEY, requestId);
response . setHeader ( "X-Request-ID" , requestId);
try {
filterChain . doFilter (request, response);
} finally {
MDC . remove (MDC_KEY);
}
}
}
The requestId is also returned in the X-Request-ID response header, even for successful requests.
Common Error Scenarios
Authentication Failures
Scenario : Token expired
Response :
{
"timestamp" : "2024-03-04T15:10:00Z" ,
"requestId" : "..." ,
"message" : "JWT token has expired" ,
"detail" : "ExpiredJwtException"
}
Solution : Request a new token via /api/auth/login or implement token refresh.
Scenario : Invalid signature
Response :
{
"timestamp" : "2024-03-04T15:15:00Z" ,
"requestId" : "..." ,
"message" : "Invalid JWT signature" ,
"detail" : "SignatureException"
}
Solution : Verify JWT_SECRET matches between token generation and validation. Check for secret rotation.
Database Connection Errors
Scenario : PostgreSQL unavailable
Response :
{
"timestamp" : "2024-03-04T15:20:00Z" ,
"requestId" : "..." ,
"message" : "Failed to connect to database" ,
"detail" : "PersistenceException"
}
Solution :
Check PostgreSQL is running: pg_isready
Verify connection string in environment variables
Check database credentials
Verify network connectivity and firewall rules
Constraint Violations
Scenario : Duplicate email
Response :
{
"timestamp" : "2024-03-04T15:25:00Z" ,
"requestId" : "..." ,
"message" : "User with email john@example.com already exists" ,
"detail" : "UserAlreadyExistsException"
}
Solution : Check if user exists before attempting to create. Use different email address.
Logging and Monitoring
Log Control Service
The LogControlService ensures audit logs are properly initialized:
private void ensureLogControl () {
try {
String path = logControlService . defaultTodayLogPath ();
logControlService . ensureForToday (path);
} catch ( Exception ex ) {
log . warn ( "No se pudo registrar LogControl: {}" , ex . getMessage (), ex);
}
}
This is called before handling validation, persistence, and generic exceptions.
Audit Logs
All operations annotated with @AuditLog are automatically logged to the activity_logs table:
SELECT * FROM activity_logs
WHERE request_id = 'a7b8c9d0-e1f2-3456-0123-567890123456'
ORDER BY timestamp DESC ;
Testing Error Scenarios
Integration Tests
@ Test
void shouldReturn400WhenEmailInvalid () {
CreateUserRequest request = new CreateUserRequest (
"John Doe" ,
"invalid-email" ,
"password123"
);
mockMvc . perform ( post ( "/api/users" )
. contentType ( MediaType . APPLICATION_JSON )
. content ( objectMapper . writeValueAsString (request)))
. andExpect ( status (). isBadRequest ())
. andExpect ( jsonPath ( "$.message" ). value ( "Validation failed" ))
. andExpect ( jsonPath ( "$.detail" ). value ( containsString ( "email" )));
. andExpect ( jsonPath ( "$.requestId" ). exists ());
}
@ Test
void shouldReturn409WhenEmailExists () {
// Given: existing user
userRepository . save (existingUser);
// When: try to create user with same email
CreateUserRequest request = new CreateUserRequest (
"Jane Doe" ,
existingUser . getEmail (),
"password123"
);
// Then: expect conflict
mockMvc . perform ( post ( "/api/users" )
. contentType ( MediaType . APPLICATION_JSON )
. content ( objectMapper . writeValueAsString (request)))
. andExpect ( status (). isConflict ())
. andExpect ( jsonPath ( "$.message" ). value ( containsString ( "already exists" )));
}
Next Steps
Configuration Guide Learn how to configure the service to prevent common errors
Security Best Practices Understand authentication and authorization errors