Skip to main content

Error Response Format

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"
}
FieldTypeDescription
timestampISO 8601 DateTimeWhen the error occurred (UTC)
requestIdUUIDUnique identifier for tracing the request in logs
messageStringHigh-level error category or message
detailStringSpecific 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:
  1. Check PostgreSQL is running: pg_isready
  2. Verify connection string in environment variables
  3. Check database credentials
  4. 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

Build docs developers (and LLMs) love