diff --git a/src/main/java/se/urmo/hemhub/web/ErrorHandling.java b/src/main/java/se/urmo/hemhub/web/ErrorHandling.java new file mode 100644 index 0000000..e984cd5 --- /dev/null +++ b/src/main/java/se/urmo/hemhub/web/ErrorHandling.java @@ -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 details){} + + private ErrorResponse resp(HttpStatus s, String message, String path, Map 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")); + } +}