Pular para o conteúdo principal

Tratamento de Erros

O Archbase fornece um sistema centralizado de tratamento de erros com exceções e respostas padronizadas.

Exceções do Framework

ExceçãoHTTP StatusQuando Usar
ResourceNotFoundException404Recurso não encontrado
ValidationException400Erro de validação
BusinessRuleException422Violação de regra de negócio
ConflictException409Conflito de dados
UnauthorizedException401Não autenticado
ForbiddenException403Sem permissão
ArchbaseException500Erro genérico

Exceções de Domínio

ResourceNotFoundException

@Service
public class ClienteService {

private final ClienteRepository repository;

public Cliente buscarPorId(UUID id) {
return repository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException(
"Cliente não encontrado",
id
));
}
}

Resposta:

{
"timestamp": "2024-12-28T10:30:00Z",
"status": 404,
"error": "Not Found",
"message": "Cliente não encontrado",
"path": "/api/v1/clientes/123",
"resourceId": "123"
}

ValidationException

@Service
public class PedidoService {

public void criar(Pedido pedido) {
ValidationResult result = pedido.validate();

if (!result.isValid()) {
throw new ValidationException(
"Erro de validação do pedido",
result.getErrors()
);
}

repository.save(pedido);
}
}

Resposta:

{
"timestamp": "2024-12-28T10:30:00Z",
"status": 400,
"error": "Bad Request",
"message": "Erro de validação do pedido",
"errors": [
{
"field": "clienteId",
"message": "Cliente é obrigatório"
},
{
"field": "itens",
"message": "Pedido deve ter ao menos um item"
}
]
}

BusinessRuleException

@Service
public class CaixaService {

public void fecharCaixa(UUID caixaId, UUID operadorId) {
Caixa caixa = caixaRepository.findById(caixaId)
.orElseThrow(() -> new ResourceNotFoundException("Caixa", caixaId));

if (!caixa.podeSerFechado()) {
throw new BusinessRuleException(
"Caixa não pode ser fechado",
"CAIXA_ABERTO",
Map.of("status", caixa.getStatus(), "saldo", caixa.getSaldo())
);
}

caixa.fechar(operadorId);
caixaRepository.save(caixa);
}
}

Resposta:

{
"timestamp": "2024-12-28T10:30:00Z",
"status": 422,
"error": "Unprocessable Entity",
"message": "Caixa não pode ser fechado",
"code": "CAIXA_ABERTO",
"details": {
"status": "ABERTO",
"saldo": 150.00
}
}

ConflictException

@Service
public class UsuarioService {

public Usuario criar(Usuario usuario) {
if (repository.existsByEmail(usuario.getEmail())) {
throw new ConflictException(
"Já existe um usuário com este e-mail",
"EMAIL_DUPLICADO",
"email"
);
}
return repository.save(usuario);
}
}

Exceções Customizadas

package com.exemplo.domain.exception;

public class EstoqueInsuficienteException extends BusinessRuleException {

public EstoqueInsuficienteException(UUID produtoId, int solicitado, int disponivel) {
super(
String.format("Estoque insuficiente para o produto %s: solicitado=%d, disponível=%d",
produtoId, solicitado, disponivel),
"ESTOQUE_INSUFICIENTE",
Map.of(
"produtoId", produtoId,
"solicitado", solicitado,
"disponivel", disponivel
)
);
}
}

Global Exception Handler

@RestControllerAdvice
public class GlobalExceptionHandler {

private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(
ResourceNotFoundException ex,
HttpServletRequest request) {

ErrorResponse response = ErrorResponse.builder()
.timestamp(Instant.now())
.status(HttpStatus.NOT_FOUND.value())
.error("Not Found")
.message(ex.getMessage())
.path(request.getRequestURI())
.resourceId(ex.getResourceId())
.build();

return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
}

@ExceptionHandler(ValidationException.class)
public ResponseEntity<ValidationErrorResponse> handleValidation(
ValidationException ex,
HttpServletRequest request) {

ValidationErrorResponse response = ValidationErrorResponse.builder()
.timestamp(Instant.now())
.status(HttpStatus.BAD_REQUEST.value())
.error("Bad Request")
.message(ex.getMessage())
.path(request.getRequestURI())
.errors(ex.getErrors())
.build();

return ResponseEntity.badRequest().body(response);
}

@ExceptionHandler(BusinessRuleException.class)
public ResponseEntity<ErrorResponse> handleBusinessRule(
BusinessRuleException ex,
HttpServletRequest request) {

ErrorResponse response = ErrorResponse.builder()
.timestamp(Instant.now())
.status(HttpStatus.UNPROCESSABLE_ENTITY.value())
.error("Unprocessable Entity")
.message(ex.getMessage())
.path(request.getRequestURI())
.code(ex.getCode())
.details(ex.getDetails())
.build();

return ResponseEntity.unprocessableEntity().body(response);
}

@ExceptionHandler(ConflictException.class)
public ResponseEntity<ErrorResponse> handleConflict(
ConflictException ex,
HttpServletRequest request) {

ErrorResponse response = ErrorResponse.builder()
.timestamp(Instant.now())
.status(HttpStatus.CONFLICT.value())
.error("Conflict")
.message(ex.getMessage())
.path(request.getRequestURI())
.code(ex.getCode())
.field(ex.getField())
.build();

return ResponseEntity.status(HttpStatus.CONFLICT).body(response);
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ValidationErrorResponse> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpServletRequest request) {

List<ValidationError> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> ValidationError.builder()
.field(error.getField())
.message(error.getDefaultMessage())
.rejectedValue(error.getRejectedValue())
.build())
.toList();

ValidationErrorResponse response = ValidationErrorResponse.builder()
.timestamp(Instant.now())
.status(HttpStatus.BAD_REQUEST.value())
.error("Bad Request")
.message("Erro de validação")
.path(request.getRequestURI())
.errors(errors)
.build();

return ResponseEntity.badRequest().body(response);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneric(
Exception ex,
HttpServletRequest request) {

logger.error("Erro não tratado", ex);

ErrorResponse response = ErrorResponse.builder()
.timestamp(Instant.now())
.status(HttpStatus.INTERNAL_SERVER_ERROR.value())
.error("Internal Server Error")
.message("Ocorreu um erro inesperado")
.path(request.getRequestURI())
.build();

return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
}

Error Response DTOs

@Data
@Builder
public class ErrorResponse {
private Instant timestamp;
private Integer status;
private String error;
private String message;
private String path;
private String code;
private String field;
private String resourceId;
private Map<String, Object> details;
}

@Data
@Builder
public class ValidationErrorResponse extends ErrorResponse {
private List<ValidationError> errors;
}

@Data
@Builder
public class ValidationError {
private String field;
private String message;
private Object rejectedValue;
}

Configuração

archbase:
error-handling:
enabled: true
include-stack-trace: false # false em produção
include-message: always
log-errors: true

Testando Exceções

@SpringBootTest
class PedidoServiceTest {

@Autowired
private PedidoService pedidoService;

@Test
void deveLancarExcecaoQuandoPedidoSemItens() {
// Arrange
CriarPedidoCommand command = new CriarPedidoCommand(
clienteId,
List.of() // sem itens
);

// Act & Assert
assertThatThrownBy(() -> pedidoService.criar(command))
.isInstanceOf(BusinessRuleException.class)
.hasMessageContaining("Pedido deve ter ao menos um item");
}

@Test
void deveLancarExcecaoQuandoClienteNaoEncontrado() {
assertThatThrownBy(() -> pedidoService.criar(
new CriarPedidoCommand(UUID.randomUUID(), List.of())
))
.isInstanceOf(ResourceNotFoundException.class)
.hasMessageContaining("Cliente não encontrado");
}
}

Boas Práticas

PráticaDescrição
Exceções específicasUse exceções de domínio específicas
Mensagens clarasMensagens que ajudam o cliente a entender o erro
Códigos de erroUse códigos para tratamento programático
Log de errosSempre logue erros não tratados
Não exponha stackEm produção, oculte stack traces

Próximos Passos