Exception handling #82
@ -8,26 +8,26 @@ import org.springframework.http.HttpStatus;
|
|||||||
*/
|
*/
|
||||||
public class ForbiddenException extends StudentportalenException {
|
public class ForbiddenException extends StudentportalenException {
|
||||||
|
|
||||||
public ForbiddenException(String message) {
|
public ForbiddenException(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ForbiddenException(String message, Throwable cause) {
|
public ForbiddenException(String message, Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getType() {
|
public String getType() {
|
||||||
return "studentportalen:forbidden";
|
return "studentportalen:forbidden";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getTitle() {
|
public String getTitle() {
|
||||||
return "Access denied";
|
return "Access denied";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HttpStatus getStatus() {
|
public HttpStatus getStatus() {
|
||||||
return HttpStatus.FORBIDDEN;
|
return HttpStatus.FORBIDDEN;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
package se.su.dsv.studentportalen.bff.exception;
|
package se.su.dsv.studentportalen.bff.exception;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
@ -9,11 +13,6 @@ import org.springframework.web.bind.MethodArgumentNotValidException;
|
|||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Centralized exception handler for all REST controllers.
|
* Centralized exception handler for all REST controllers.
|
||||||
* Returns RFC 7807 Problem Details for all error responses.
|
* Returns RFC 7807 Problem Details for all error responses.
|
||||||
@ -21,65 +20,77 @@ import java.util.stream.Collectors;
|
|||||||
@RestControllerAdvice
|
@RestControllerAdvice
|
||||||
public class GlobalExceptionHandler {
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
private static final Logger LOG = LoggerFactory.getLogger(
|
||||||
|
GlobalExceptionHandler.class
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles Jakarta Bean Validation errors (missing/invalid fields).
|
* Handles Jakarta Bean Validation errors (missing/invalid fields).
|
||||||
* Triggered when @Valid fails on request body.
|
* Triggered when @Valid fails on request body.
|
||||||
*/
|
*/
|
||||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
public ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
|
public ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
|
||||||
Map<String, List<String>> violations = ex.getBindingResult().getFieldErrors().stream()
|
Map<String, List<String>> violations = ex
|
||||||
.collect(Collectors.groupingBy(
|
.getBindingResult()
|
||||||
FieldError::getField,
|
.getFieldErrors()
|
||||||
Collectors.mapping(
|
.stream()
|
||||||
error -> error.getDefaultMessage() != null ? error.getDefaultMessage() : "Invalid value",
|
.collect(
|
||||||
Collectors.toList()
|
Collectors.groupingBy(
|
||||||
)
|
FieldError::getField,
|
||||||
));
|
Collectors.mapping(
|
||||||
|
error ->
|
||||||
|
error.getDefaultMessage() != null
|
||||||
|
? error.getDefaultMessage()
|
||||||
|
: "Invalid value",
|
||||||
|
Collectors.toList()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
LOG.warn("Validation failed: {}", violations);
|
LOG.warn("Validation failed: {}", violations);
|
||||||
|
|
||||||
ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
|
ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
|
||||||
problem.setType(URI.create("studentportalen:validation-error"));
|
problem.setType(URI.create("studentportalen:validation-error"));
|
||||||
problem.setTitle("Validation failed");
|
problem.setTitle("Validation failed");
|
||||||
problem.setDetail("One or more fields are invalid");
|
problem.setDetail("One or more fields are invalid");
|
||||||
problem.setProperty("violations", violations);
|
problem.setProperty("violations", violations);
|
||||||
return problem;
|
return problem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles business exceptions from the BFF.
|
||||||
|
* These include mapped errors from backend systems like Daisy.
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(StudentportalenException.class)
|
||||||
|
public ProblemDetail handleBusinessError(StudentportalenException ex) {
|
||||||
|
LOG.warn("Business error: {} - {}", ex.getType(), ex.getMessage());
|
||||||
|
|
||||||
|
ProblemDetail problem = ProblemDetail.forStatus(ex.getStatus());
|
||||||
|
problem.setType(URI.create(ex.getType()));
|
||||||
|
problem.setTitle(ex.getTitle());
|
||||||
|
problem.setDetail(ex.getMessage());
|
||||||
|
|
||||||
|
if (ex instanceof ValidationException ve && !ve.getViolations().isEmpty()) {
|
||||||
|
problem.setProperty("violations", ve.getViolations());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
return problem;
|
||||||
* Handles business exceptions from the BFF.
|
}
|
||||||
* These include mapped errors from backend systems like Daisy.
|
|
||||||
*/
|
|
||||||
@ExceptionHandler(StudentportalenException.class)
|
|
||||||
public ProblemDetail handleBusinessError(StudentportalenException ex) {
|
|
||||||
LOG.warn("Business error: {} - {}", ex.getType(), ex.getMessage());
|
|
||||||
|
|
||||||
ProblemDetail problem = ProblemDetail.forStatus(ex.getStatus());
|
/**
|
||||||
problem.setType(URI.create(ex.getType()));
|
* Handles unexpected errors.
|
||||||
problem.setTitle(ex.getTitle());
|
* Logs the full stack trace but returns a generic message to the client.
|
||||||
problem.setDetail(ex.getMessage());
|
*/
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ProblemDetail handleUnexpected(Exception ex) {
|
||||||
|
LOG.error("Unexpected error", ex);
|
||||||
|
|
||||||
if (ex instanceof ValidationException ve && !ve.getViolations().isEmpty()) {
|
ProblemDetail problem = ProblemDetail.forStatus(
|
||||||
problem.setProperty("violations", ve.getViolations());
|
HttpStatus.INTERNAL_SERVER_ERROR
|
||||||
}
|
);
|
||||||
|
problem.setType(URI.create("studentportalen:internal-error"));
|
||||||
return problem;
|
problem.setTitle("An unexpected error occurred");
|
||||||
}
|
problem.setDetail("Please try again later");
|
||||||
|
return problem;
|
||||||
/**
|
}
|
||||||
* Handles unexpected errors.
|
|
||||||
* Logs the full stack trace but returns a generic message to the client.
|
|
||||||
*/
|
|
||||||
@ExceptionHandler(Exception.class)
|
|
||||||
public ProblemDetail handleUnexpected(Exception ex) {
|
|
||||||
LOG.error("Unexpected error", ex);
|
|
||||||
|
|
||||||
ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.INTERNAL_SERVER_ERROR);
|
|
||||||
problem.setType(URI.create("studentportalen:internal-error"));
|
|
||||||
problem.setTitle("An unexpected error occurred");
|
|
||||||
problem.setDetail("Please try again later");
|
|
||||||
return problem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,26 +8,26 @@ import org.springframework.http.HttpStatus;
|
|||||||
*/
|
*/
|
||||||
public class ServiceUnavailableException extends StudentportalenException {
|
public class ServiceUnavailableException extends StudentportalenException {
|
||||||
|
|
||||||
public ServiceUnavailableException(String message) {
|
public ServiceUnavailableException(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServiceUnavailableException(String message, Throwable cause) {
|
public ServiceUnavailableException(String message, Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getType() {
|
public String getType() {
|
||||||
return "studentportalen:service-unavailable";
|
return "studentportalen:service-unavailable";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getTitle() {
|
public String getTitle() {
|
||||||
return "Service unavailable";
|
return "Service unavailable";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HttpStatus getStatus() {
|
public HttpStatus getStatus() {
|
||||||
return HttpStatus.SERVICE_UNAVAILABLE;
|
return HttpStatus.SERVICE_UNAVAILABLE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,27 +8,27 @@ import org.springframework.http.HttpStatus;
|
|||||||
*/
|
*/
|
||||||
public abstract class StudentportalenException extends RuntimeException {
|
public abstract class StudentportalenException extends RuntimeException {
|
||||||
|
|
||||||
protected StudentportalenException(String message) {
|
protected StudentportalenException(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected StudentportalenException(String message, Throwable cause) {
|
protected StudentportalenException(String message, Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The error type URI for RFC 7807 Problem Details.
|
* The error type URI for RFC 7807 Problem Details.
|
||||||
* Example: "studentportalen:validation-error"
|
* Example: "studentportalen:validation-error"
|
||||||
*/
|
*/
|
||||||
public abstract String getType();
|
public abstract String getType();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A short, human-readable summary of the problem type.
|
* A short, human-readable summary of the problem type.
|
||||||
*/
|
*/
|
||||||
public abstract String getTitle();
|
public abstract String getTitle();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The HTTP status code for this error.
|
* The HTTP status code for this error.
|
||||||
*/
|
*/
|
||||||
public abstract HttpStatus getStatus();
|
public abstract HttpStatus getStatus();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
package se.su.dsv.studentportalen.bff.exception;
|
package se.su.dsv.studentportalen.bff.exception;
|
||||||
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exception for validation errors, including field-level violations.
|
* Exception for validation errors, including field-level violations.
|
||||||
@ -11,37 +10,40 @@ import java.util.Map;
|
|||||||
*/
|
*/
|
||||||
public class ValidationException extends StudentportalenException {
|
public class ValidationException extends StudentportalenException {
|
||||||
|
|
||||||
private final Map<String, List<String>> violations;
|
private final Map<String, List<String>> violations;
|
||||||
|
|
||||||
public ValidationException(String message) {
|
public ValidationException(String message) {
|
||||||
this(message, Map.of());
|
this(message, Map.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValidationException(String message, Map<String, List<String>> violations) {
|
public ValidationException(
|
||||||
super(message);
|
String message,
|
||||||
this.violations = violations != null ? Map.copyOf(violations) : Map.of();
|
Map<String, List<String>> violations
|
||||||
}
|
) {
|
||||||
|
super(message);
|
||||||
|
this.violations = violations != null ? Map.copyOf(violations) : Map.of();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getType() {
|
public String getType() {
|
||||||
return "studentportalen:validation-error";
|
return "studentportalen:validation-error";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getTitle() {
|
public String getTitle() {
|
||||||
return "Validation failed";
|
return "Validation failed";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HttpStatus getStatus() {
|
public HttpStatus getStatus() {
|
||||||
return HttpStatus.BAD_REQUEST;
|
return HttpStatus.BAD_REQUEST;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Field-level validation errors.
|
* Field-level validation errors.
|
||||||
* Keys are field names, values are lists of error messages.
|
* Keys are field names, values are lists of error messages.
|
||||||
*/
|
*/
|
||||||
public Map<String, List<String>> getViolations() {
|
public Map<String, List<String>> getViolations() {
|
||||||
return violations;
|
return violations;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user