Global error handling

This commit is contained in:
Urban Modig
2025-10-08 19:53:32 +02:00
parent e0d041ef67
commit c8dd022395

View File

@ -0,0 +1,86 @@
package se.urmo.hemhub.web;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolationException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import java.time.Instant;
import java.util.Map;
import java.util.NoSuchElementException;
@RestControllerAdvice
public class ErrorHandling {
record ErrorResponse(Instant timestamp, int status, String error, String message, String path, Map<String,Object> details){}
private ErrorResponse resp(HttpStatus s, String message, String path, Map<String,Object> details) {
return new ErrorResponse(Instant.now(), s.value(), s.getReasonPhrase(), message, path, details);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
ErrorResponse handleValidation(MethodArgumentNotValidException ex, HttpServletRequest req) {
var fieldErrors = ex.getBindingResult().getFieldErrors().stream()
.collect(java.util.stream.Collectors.toMap(
fe -> fe.getField(),
fe -> fe.getDefaultMessage() != null ? fe.getDefaultMessage() : "Invalid value",
(a,b) -> a));
return resp(HttpStatus.BAD_REQUEST, "Validation failed", req.getRequestURI(), Map.of("fields", fieldErrors));
}
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
ErrorResponse handleConstraintViolation(ConstraintViolationException ex, HttpServletRequest req) {
var errs = ex.getConstraintViolations().stream()
.collect(java.util.stream.Collectors.toMap(
v -> v.getPropertyPath().toString(),
v -> v.getMessage(),
(a,b)->a));
return resp(HttpStatus.BAD_REQUEST, "Validation failed", req.getRequestURI(), Map.of("violations", errs));
}
@ExceptionHandler({ HttpMessageNotReadableException.class })
@ResponseStatus(HttpStatus.BAD_REQUEST)
ErrorResponse handleBadBody(Exception ex, HttpServletRequest req) {
return resp(HttpStatus.BAD_REQUEST, "Malformed request body", req.getRequestURI(), Map.of());
}
@ExceptionHandler(NoSuchElementException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
ErrorResponse handleNotFound(NoSuchElementException ex, HttpServletRequest req) {
return resp(HttpStatus.NOT_FOUND, "Not found", req.getRequestURI(), Map.of());
}
@ExceptionHandler({ SecurityException.class, AccessDeniedException.class })
@ResponseStatus(HttpStatus.FORBIDDEN)
ErrorResponse handleForbidden(Exception ex, HttpServletRequest req) {
return resp(HttpStatus.FORBIDDEN, "Forbidden", req.getRequestURI(), Map.of());
}
@ExceptionHandler(DataIntegrityViolationException.class)
@ResponseStatus(HttpStatus.CONFLICT)
ErrorResponse handleConflict(DataIntegrityViolationException ex, HttpServletRequest req) {
return resp(HttpStatus.CONFLICT, "Conflict", req.getRequestURI(), Map.of());
}
@ExceptionHandler({ HttpRequestMethodNotSupportedException.class, HttpMediaTypeNotSupportedException.class })
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
ErrorResponse handleMethod(Exception ex, HttpServletRequest req) {
return resp(HttpStatus.METHOD_NOT_ALLOWED, "Method not allowed", req.getRequestURI(), Map.of());
}
// Fallback (kept last)
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
ErrorResponse handleOther(Exception ex, HttpServletRequest req) {
return resp(HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error", req.getRequestURI(),
Map.of("hint","Check server logs"));
}
}